diff --git a/cmd/geth/les_test.go b/cmd/geth/les_test.go
index ae9ed5ecf5341ee467eb9fc90503138203d96ed3..259d4a8067373625c5b523d366beeee7f8bc3693 100644
--- a/cmd/geth/les_test.go
+++ b/cmd/geth/les_test.go
@@ -152,7 +152,7 @@ func TestPriorityClient(t *testing.T) {
 	defer prioCli.killAndWait()
 	// 3_000_000_000 once we move to Go 1.13
 	tokens := 3000000000
-	lightServer.callRPC(nil, "les_addBalance", prioCli.getNodeInfo().ID, tokens, "foobar")
+	lightServer.callRPC(nil, "les_addBalance", prioCli.getNodeInfo().ID, tokens)
 	prioCli.addPeer(lightServer)
 
 	// Check if priority client is actually syncing and the regular client got kicked out
diff --git a/common/prque/lazyqueue.go b/common/prque/lazyqueue.go
index 92ddd77f677a15b64f7ef95378517026336b9753..52403df46497c5f976ba99de732ce63c29d5e90c 100644
--- a/common/prque/lazyqueue.go
+++ b/common/prque/lazyqueue.go
@@ -36,14 +36,15 @@ type LazyQueue struct {
 	// Items are stored in one of two internal queues ordered by estimated max
 	// priority until the next and the next-after-next refresh. Update and Refresh
 	// always places items in queue[1].
-	queue       [2]*sstack
-	popQueue    *sstack
-	period      time.Duration
-	maxUntil    mclock.AbsTime
-	indexOffset int
-	setIndex    SetIndexCallback
-	priority    PriorityCallback
-	maxPriority MaxPriorityCallback
+	queue                      [2]*sstack
+	popQueue                   *sstack
+	period                     time.Duration
+	maxUntil                   mclock.AbsTime
+	indexOffset                int
+	setIndex                   SetIndexCallback
+	priority                   PriorityCallback
+	maxPriority                MaxPriorityCallback
+	lastRefresh1, lastRefresh2 mclock.AbsTime
 }
 
 type (
@@ -54,14 +55,17 @@ type (
 // NewLazyQueue creates a new lazy queue
 func NewLazyQueue(setIndex SetIndexCallback, priority PriorityCallback, maxPriority MaxPriorityCallback, clock mclock.Clock, refreshPeriod time.Duration) *LazyQueue {
 	q := &LazyQueue{
-		popQueue:    newSstack(nil),
-		setIndex:    setIndex,
-		priority:    priority,
-		maxPriority: maxPriority,
-		clock:       clock,
-		period:      refreshPeriod}
+		popQueue:     newSstack(nil),
+		setIndex:     setIndex,
+		priority:     priority,
+		maxPriority:  maxPriority,
+		clock:        clock,
+		period:       refreshPeriod,
+		lastRefresh1: clock.Now(),
+		lastRefresh2: clock.Now(),
+	}
 	q.Reset()
-	q.Refresh()
+	q.refresh(clock.Now())
 	return q
 }
 
@@ -71,9 +75,19 @@ func (q *LazyQueue) Reset() {
 	q.queue[1] = newSstack(q.setIndex1)
 }
 
-// Refresh should be called at least with the frequency specified by the refreshPeriod parameter
+// Refresh performs queue re-evaluation if necessary
 func (q *LazyQueue) Refresh() {
-	q.maxUntil = q.clock.Now() + mclock.AbsTime(q.period)
+	now := q.clock.Now()
+	for time.Duration(now-q.lastRefresh2) >= q.period*2 {
+		q.refresh(now)
+		q.lastRefresh2 = q.lastRefresh1
+		q.lastRefresh1 = now
+	}
+}
+
+// refresh re-evaluates items in the older queue and swaps the two queues
+func (q *LazyQueue) refresh(now mclock.AbsTime) {
+	q.maxUntil = now + mclock.AbsTime(q.period)
 	for q.queue[0].Len() != 0 {
 		q.Push(heap.Pop(q.queue[0]).(*item).value)
 	}
@@ -139,6 +153,7 @@ func (q *LazyQueue) MultiPop(callback func(data interface{}, priority int64) boo
 				}
 				return
 			}
+			nextIndex = q.peekIndex() // re-check because callback is allowed to push items back
 		}
 	}
 }
diff --git a/internal/web3ext/web3ext.go b/internal/web3ext/web3ext.go
index 41d8657787b4e15f409370fa8c12a01380a78f99..300c2b054f38a0c58cadd7b10c9645a2d6f6efb3 100644
--- a/internal/web3ext/web3ext.go
+++ b/internal/web3ext/web3ext.go
@@ -844,7 +844,7 @@ web3._extend({
 		new web3._extend.Method({
 			name: 'addBalance',
 			call: 'les_addBalance',
-			params: 3
+			params: 2
 		}),
 	],
 	properties:
diff --git a/les/api.go b/les/api.go
index cd5c99a5f1afced40c7d5638f2f6b37848bc4690..66d133b8540a1fce4aac792f4543b37fa27f2be4 100644
--- a/les/api.go
+++ b/les/api.go
@@ -19,11 +19,11 @@ package les
 import (
 	"errors"
 	"fmt"
-	"math"
 	"time"
 
 	"github.com/ethereum/go-ethereum/common/hexutil"
 	"github.com/ethereum/go-ethereum/common/mclock"
+	lps "github.com/ethereum/go-ethereum/les/lespay/server"
 	"github.com/ethereum/go-ethereum/p2p/enode"
 )
 
@@ -31,16 +31,13 @@ var (
 	errNoCheckpoint         = errors.New("no local checkpoint provided")
 	errNotActivated         = errors.New("checkpoint registrar is not activated")
 	errUnknownBenchmarkType = errors.New("unknown benchmark type")
-	errBalanceOverflow      = errors.New("balance overflow")
 	errNoPriority           = errors.New("priority too low to raise capacity")
 )
 
-const maxBalance = math.MaxInt64
-
 // PrivateLightServerAPI provides an API to access the LES light server.
 type PrivateLightServerAPI struct {
 	server                               *LesServer
-	defaultPosFactors, defaultNegFactors priceFactors
+	defaultPosFactors, defaultNegFactors lps.PriceFactors
 }
 
 // NewPrivateLightServerAPI creates a new LES light server API.
@@ -57,7 +54,6 @@ func (api *PrivateLightServerAPI) ServerInfo() map[string]interface{} {
 	res := make(map[string]interface{})
 	res["minimumCapacity"] = api.server.minCapacity
 	res["maximumCapacity"] = api.server.maxCapacity
-	res["freeClientCapacity"] = api.server.freeCapacity
 	res["totalCapacity"], res["totalConnectedCapacity"], res["priorityConnectedCapacity"] = api.server.clientPool.capacityInfo()
 	return res
 }
@@ -65,9 +61,8 @@ func (api *PrivateLightServerAPI) ServerInfo() map[string]interface{} {
 // ClientInfo returns information about clients listed in the ids list or matching the given tags
 func (api *PrivateLightServerAPI) ClientInfo(ids []enode.ID) map[enode.ID]map[string]interface{} {
 	res := make(map[enode.ID]map[string]interface{})
-	api.server.clientPool.forClients(ids, func(client *clientInfo, id enode.ID) error {
-		res[id] = api.clientInfo(client, id)
-		return nil
+	api.server.clientPool.forClients(ids, func(client *clientInfo) {
+		res[client.node.ID()] = api.clientInfo(client)
 	})
 	return res
 }
@@ -80,48 +75,40 @@ func (api *PrivateLightServerAPI) ClientInfo(ids []enode.ID) map[enode.ID]map[st
 // assigned to it.
 func (api *PrivateLightServerAPI) PriorityClientInfo(start, stop enode.ID, maxCount int) map[enode.ID]map[string]interface{} {
 	res := make(map[enode.ID]map[string]interface{})
-	ids := api.server.clientPool.ndb.getPosBalanceIDs(start, stop, maxCount+1)
+	ids := api.server.clientPool.bt.GetPosBalanceIDs(start, stop, maxCount+1)
 	if len(ids) > maxCount {
 		res[ids[maxCount]] = make(map[string]interface{})
 		ids = ids[:maxCount]
 	}
 	if len(ids) != 0 {
-		api.server.clientPool.forClients(ids, func(client *clientInfo, id enode.ID) error {
-			res[id] = api.clientInfo(client, id)
-			return nil
+		api.server.clientPool.forClients(ids, func(client *clientInfo) {
+			res[client.node.ID()] = api.clientInfo(client)
 		})
 	}
 	return res
 }
 
 // clientInfo creates a client info data structure
-func (api *PrivateLightServerAPI) clientInfo(c *clientInfo, id enode.ID) map[string]interface{} {
+func (api *PrivateLightServerAPI) clientInfo(c *clientInfo) map[string]interface{} {
 	info := make(map[string]interface{})
-	if c != nil {
-		now := mclock.Now()
-		info["isConnected"] = true
-		info["connectionTime"] = float64(now-c.connectedAt) / float64(time.Second)
-		info["capacity"] = c.capacity
-		pb, nb := c.balanceTracker.getBalance(now)
-		info["pricing/balance"], info["pricing/negBalance"] = pb, nb
-		info["pricing/balanceMeta"] = c.balanceMetaInfo
-		info["priority"] = pb != 0
-	} else {
-		info["isConnected"] = false
-		pb := api.server.clientPool.ndb.getOrNewPB(id)
-		info["pricing/balance"], info["pricing/balanceMeta"] = pb.value, pb.meta
-		info["priority"] = pb.value != 0
+	pb, nb := c.balance.GetBalance()
+	info["isConnected"] = c.connected
+	info["pricing/balance"] = pb
+	info["priority"] = pb != 0
+	//		cb := api.server.clientPool.ndb.getCurrencyBalance(id)
+	//		info["pricing/currency"] = cb.amount
+	if c.connected {
+		info["connectionTime"] = float64(mclock.Now()-c.connectedAt) / float64(time.Second)
+		info["capacity"], _ = api.server.clientPool.ns.GetField(c.node, priorityPoolSetup.CapacityField).(uint64)
+		info["pricing/negBalance"] = nb
 	}
 	return info
 }
 
 // setParams either sets the given parameters for a single connected client (if specified)
 // or the default parameters applicable to clients connected in the future
-func (api *PrivateLightServerAPI) setParams(params map[string]interface{}, client *clientInfo, posFactors, negFactors *priceFactors) (updateFactors bool, err error) {
+func (api *PrivateLightServerAPI) setParams(params map[string]interface{}, client *clientInfo, posFactors, negFactors *lps.PriceFactors) (updateFactors bool, err error) {
 	defParams := client == nil
-	if !defParams {
-		posFactors, negFactors = &client.posFactors, &client.negFactors
-	}
 	for name, value := range params {
 		errValue := func() error {
 			return fmt.Errorf("invalid value for parameter '%s'", name)
@@ -137,20 +124,20 @@ func (api *PrivateLightServerAPI) setParams(params map[string]interface{}, clien
 
 		switch {
 		case name == "pricing/timeFactor":
-			setFactor(&posFactors.timeFactor)
+			setFactor(&posFactors.TimeFactor)
 		case name == "pricing/capacityFactor":
-			setFactor(&posFactors.capacityFactor)
+			setFactor(&posFactors.CapacityFactor)
 		case name == "pricing/requestCostFactor":
-			setFactor(&posFactors.requestFactor)
+			setFactor(&posFactors.RequestFactor)
 		case name == "pricing/negative/timeFactor":
-			setFactor(&negFactors.timeFactor)
+			setFactor(&negFactors.TimeFactor)
 		case name == "pricing/negative/capacityFactor":
-			setFactor(&negFactors.capacityFactor)
+			setFactor(&negFactors.CapacityFactor)
 		case name == "pricing/negative/requestCostFactor":
-			setFactor(&negFactors.requestFactor)
+			setFactor(&negFactors.RequestFactor)
 		case !defParams && name == "capacity":
 			if capacity, ok := value.(float64); ok && uint64(capacity) >= api.server.minCapacity {
-				err = api.server.clientPool.setCapacity(client, uint64(capacity))
+				_, err = api.server.clientPool.setCapacity(client.node, client.address, uint64(capacity), 0, true)
 				// Don't have to call factor update explicitly. It's already done
 				// in setCapacity function.
 			} else {
@@ -170,27 +157,25 @@ func (api *PrivateLightServerAPI) setParams(params map[string]interface{}, clien
 	return
 }
 
-// AddBalance updates the balance of a client (either overwrites it or adds to it).
-// It also updates the balance meta info string.
-func (api *PrivateLightServerAPI) AddBalance(id enode.ID, value int64, meta string) ([2]uint64, error) {
-	oldBalance, newBalance, err := api.server.clientPool.addBalance(id, value, meta)
-	return [2]uint64{oldBalance, newBalance}, err
-}
-
 // SetClientParams sets client parameters for all clients listed in the ids list
 // or all connected clients if the list is empty
 func (api *PrivateLightServerAPI) SetClientParams(ids []enode.ID, params map[string]interface{}) error {
-	return api.server.clientPool.forClients(ids, func(client *clientInfo, id enode.ID) error {
-		if client != nil {
-			update, err := api.setParams(params, client, nil, nil)
+	var err error
+	api.server.clientPool.forClients(ids, func(client *clientInfo) {
+		if client.connected {
+			posFactors, negFactors := client.balance.GetPriceFactors()
+			update, e := api.setParams(params, client, &posFactors, &negFactors)
 			if update {
-				client.updatePriceFactors()
+				client.balance.SetPriceFactors(posFactors, negFactors)
+			}
+			if e != nil {
+				err = e
 			}
-			return err
 		} else {
-			return fmt.Errorf("client %064x is not connected", id[:])
+			err = fmt.Errorf("client %064x is not connected", client.node.ID())
 		}
 	})
+	return err
 }
 
 // SetDefaultParams sets the default parameters applicable to clients connected in the future
@@ -214,6 +199,15 @@ func (api *PrivateLightServerAPI) SetConnectedBias(bias time.Duration) error {
 	return nil
 }
 
+// AddBalance adds the given amount to the balance of a client if possible and returns
+// the balance before and after the operation
+func (api *PrivateLightServerAPI) AddBalance(id enode.ID, amount int64) (balance [2]uint64, err error) {
+	api.server.clientPool.forClients([]enode.ID{id}, func(c *clientInfo) {
+		balance[0], balance[1], err = c.balance.AddBalance(amount)
+	})
+	return
+}
+
 // Benchmark runs a request performance benchmark with a given set of measurement setups
 // in multiple passes specified by passCount. The measurement time for each setup in each
 // pass is specified in milliseconds by length.
@@ -304,13 +298,15 @@ func NewPrivateDebugAPI(server *LesServer) *PrivateDebugAPI {
 
 // FreezeClient forces a temporary client freeze which normally happens when the server is overloaded
 func (api *PrivateDebugAPI) FreezeClient(id enode.ID) error {
-	return api.server.clientPool.forClients([]enode.ID{id}, func(c *clientInfo, id enode.ID) error {
-		if c == nil {
-			return fmt.Errorf("client %064x is not connected", id[:])
+	var err error
+	api.server.clientPool.forClients([]enode.ID{id}, func(c *clientInfo) {
+		if c.connected {
+			c.peer.freeze()
+		} else {
+			err = fmt.Errorf("client %064x is not connected", id[:])
 		}
-		c.peer.freezeClient()
-		return nil
 	})
+	return err
 }
 
 // PrivateLightAPI provides an API to access the LES light server or light client.
diff --git a/les/api_test.go b/les/api_test.go
index 7f6aca55cc5adfac4053e5c111ab48deccbc5d68..2895264f67edaf5c6b0a983237d695f06c0a14de 100644
--- a/les/api_test.go
+++ b/les/api_test.go
@@ -107,7 +107,7 @@ func testCapacityAPI(t *testing.T, clientCount int) {
 			t.Fatalf("Failed to obtain rpc client: %v", err)
 		}
 		headNum, headHash := getHead(ctx, t, serverRpcClient)
-		minCap, freeCap, totalCap := getCapacityInfo(ctx, t, serverRpcClient)
+		minCap, totalCap := getCapacityInfo(ctx, t, serverRpcClient)
 		testCap := totalCap * 3 / 4
 		t.Logf("Server testCap: %d  minCap: %d  head number: %d  head hash: %064x\n", testCap, minCap, headNum, headHash)
 		reqMinCap := uint64(float64(testCap) * minRelCap / (minRelCap + float64(len(clients)-1)))
@@ -202,7 +202,7 @@ func testCapacityAPI(t *testing.T, clientCount int) {
 
 		weights := make([]float64, len(clients))
 		for c := 0; c < 5; c++ {
-			setCapacity(ctx, t, serverRpcClient, clients[freeIdx].ID(), freeCap)
+			setCapacity(ctx, t, serverRpcClient, clients[freeIdx].ID(), minCap)
 			freeIdx = rand.Intn(len(clients))
 			var sum float64
 			for i := range clients {
@@ -214,7 +214,7 @@ func testCapacityAPI(t *testing.T, clientCount int) {
 				sum += weights[i]
 			}
 			for i, client := range clients {
-				weights[i] *= float64(testCap-freeCap-100) / sum
+				weights[i] *= float64(testCap-minCap-100) / sum
 				capacity := uint64(weights[i])
 				if i != freeIdx && capacity < getCapacity(ctx, t, serverRpcClient, client.ID()) {
 					setCapacity(ctx, t, serverRpcClient, client.ID(), capacity)
@@ -227,7 +227,7 @@ func testCapacityAPI(t *testing.T, clientCount int) {
 					setCapacity(ctx, t, serverRpcClient, client.ID(), capacity)
 				}
 			}
-			weights[freeIdx] = float64(freeCap)
+			weights[freeIdx] = float64(minCap)
 			for i := range clients {
 				weights[i] /= float64(testCap)
 			}
@@ -247,7 +247,7 @@ func testCapacityAPI(t *testing.T, clientCount int) {
 				default:
 				}
 
-				_, _, totalCap = getCapacityInfo(ctx, t, serverRpcClient)
+				_, totalCap = getCapacityInfo(ctx, t, serverRpcClient)
 				if totalCap < testCap {
 					t.Log("Total capacity underrun")
 					close(stop)
@@ -370,7 +370,7 @@ func getCapacity(ctx context.Context, t *testing.T, server *rpc.Client, clientID
 	return uint64(vv)
 }
 
-func getCapacityInfo(ctx context.Context, t *testing.T, server *rpc.Client) (minCap, freeCap, totalCap uint64) {
+func getCapacityInfo(ctx context.Context, t *testing.T, server *rpc.Client) (minCap, totalCap uint64) {
 	var res map[string]interface{}
 	if err := server.CallContext(ctx, &res, "les_serverInfo"); err != nil {
 		t.Fatalf("Failed to query server info: %v", err)
@@ -387,7 +387,6 @@ func getCapacityInfo(ctx context.Context, t *testing.T, server *rpc.Client) (min
 		return uint64(vv)
 	}
 	minCap = decode("minimumCapacity")
-	freeCap = decode("freeClientCapacity")
 	totalCap = decode("totalCapacity")
 	return
 }
diff --git a/les/balance.go b/les/balance.go
deleted file mode 100644
index 51cef15c803db71d61290a9c9f1a45e19f595f1d..0000000000000000000000000000000000000000
--- a/les/balance.go
+++ /dev/null
@@ -1,389 +0,0 @@
-// Copyright 2019 The go-ethereum Authors
-// This file is part of the go-ethereum library.
-//
-// The go-ethereum library is free software: you can redistribute it and/or modify
-// it under the terms of the GNU Lesser General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
-//
-// The go-ethereum library is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU Lesser General Public License for more details.
-//
-// You should have received a copy of the GNU Lesser General Public License
-// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
-
-package les
-
-import (
-	"sync"
-	"time"
-
-	"github.com/ethereum/go-ethereum/common/mclock"
-)
-
-const (
-	balanceCallbackQueue = iota
-	balanceCallbackZero
-	balanceCallbackCount
-)
-
-// balanceTracker keeps track of the positive and negative balances of a connected
-// client and calculates actual and projected future priority values required by
-// prque.LazyQueue.
-type balanceTracker struct {
-	lock                             sync.Mutex
-	clock                            mclock.Clock
-	stopped                          bool
-	capacity                         uint64
-	balance                          balance
-	timeFactor, requestFactor        float64
-	negTimeFactor, negRequestFactor  float64
-	sumReqCost                       uint64
-	lastUpdate, nextUpdate, initTime mclock.AbsTime
-	updateEvent                      mclock.Timer
-	// since only a limited and fixed number of callbacks are needed, they are
-	// stored in a fixed size array ordered by priority threshold.
-	callbacks [balanceCallbackCount]balanceCallback
-	// callbackIndex maps balanceCallback constants to callbacks array indexes (-1 if not active)
-	callbackIndex [balanceCallbackCount]int
-	callbackCount int // number of active callbacks
-}
-
-// balance represents a pair of positive and negative balances
-type balance struct {
-	pos, neg uint64
-}
-
-// balanceCallback represents a single callback that is activated when client priority
-// reaches the given threshold
-type balanceCallback struct {
-	id        int
-	threshold int64
-	callback  func()
-}
-
-// init initializes balanceTracker
-func (bt *balanceTracker) init(clock mclock.Clock, capacity uint64) {
-	bt.clock = clock
-	bt.initTime, bt.lastUpdate = clock.Now(), clock.Now() // Init timestamps
-	for i := range bt.callbackIndex {
-		bt.callbackIndex[i] = -1
-	}
-	bt.capacity = capacity
-}
-
-// stop shuts down the balance tracker
-func (bt *balanceTracker) stop(now mclock.AbsTime) {
-	bt.lock.Lock()
-	defer bt.lock.Unlock()
-
-	bt.stopped = true
-	bt.addBalance(now)
-	bt.negTimeFactor = 0
-	bt.negRequestFactor = 0
-	bt.timeFactor = 0
-	bt.requestFactor = 0
-	if bt.updateEvent != nil {
-		bt.updateEvent.Stop()
-		bt.updateEvent = nil
-	}
-}
-
-// balanceToPriority converts a balance to a priority value. Higher priority means
-// first to disconnect. Positive balance translates to negative priority. If positive
-// balance is zero then negative balance translates to a positive priority.
-func (bt *balanceTracker) balanceToPriority(b balance) int64 {
-	if b.pos > 0 {
-		return ^int64(b.pos / bt.capacity)
-	}
-	return int64(b.neg)
-}
-
-// reducedBalance estimates the reduced balance at a given time in the fututre based
-// on the current balance, the time factor and an estimated average request cost per time ratio
-func (bt *balanceTracker) reducedBalance(at mclock.AbsTime, avgReqCost float64) balance {
-	dt := float64(at - bt.lastUpdate)
-	b := bt.balance
-	if b.pos != 0 {
-		factor := bt.timeFactor + bt.requestFactor*avgReqCost
-		diff := uint64(dt * factor)
-		if diff <= b.pos {
-			b.pos -= diff
-			dt = 0
-		} else {
-			dt -= float64(b.pos) / factor
-			b.pos = 0
-		}
-	}
-	if dt != 0 {
-		factor := bt.negTimeFactor + bt.negRequestFactor*avgReqCost
-		b.neg += uint64(dt * factor)
-	}
-	return b
-}
-
-// timeUntil calculates the remaining time needed to reach a given priority level
-// assuming that no requests are processed until then. If the given level is never
-// reached then (0, false) is returned.
-// Note: the function assumes that the balance has been recently updated and
-// calculates the time starting from the last update.
-func (bt *balanceTracker) timeUntil(priority int64) (time.Duration, bool) {
-	var dt float64
-	if bt.balance.pos != 0 {
-		if bt.timeFactor < 1e-100 {
-			return 0, false
-		}
-		if priority < 0 {
-			newBalance := uint64(^priority) * bt.capacity
-			if newBalance > bt.balance.pos {
-				return 0, false
-			}
-			dt = float64(bt.balance.pos-newBalance) / bt.timeFactor
-			return time.Duration(dt), true
-		} else {
-			dt = float64(bt.balance.pos) / bt.timeFactor
-		}
-	} else {
-		if priority < 0 {
-			return 0, false
-		}
-	}
-	// if we have a positive balance then dt equals the time needed to get it to zero
-	if uint64(priority) > bt.balance.neg {
-		if bt.negTimeFactor < 1e-100 {
-			return 0, false
-		}
-		dt += float64(uint64(priority)-bt.balance.neg) / bt.negTimeFactor
-	}
-	return time.Duration(dt), true
-}
-
-// setCapacity updates the capacity value used for priority calculation
-func (bt *balanceTracker) setCapacity(capacity uint64) {
-	bt.lock.Lock()
-	defer bt.lock.Unlock()
-
-	bt.capacity = capacity
-}
-
-// getPriority returns the actual priority based on the current balance
-func (bt *balanceTracker) getPriority(now mclock.AbsTime) int64 {
-	bt.lock.Lock()
-	defer bt.lock.Unlock()
-
-	bt.addBalance(now)
-	return bt.balanceToPriority(bt.balance)
-}
-
-// estimatedPriority gives an upper estimate for the priority at a given time in the future.
-// If addReqCost is true then an average request cost per time is assumed that is twice the
-// average cost per time in the current session. If false, zero request cost is assumed.
-func (bt *balanceTracker) estimatedPriority(at mclock.AbsTime, addReqCost bool) int64 {
-	bt.lock.Lock()
-	defer bt.lock.Unlock()
-
-	var avgReqCost float64
-	if addReqCost {
-		dt := time.Duration(bt.lastUpdate - bt.initTime)
-		if dt > time.Second {
-			avgReqCost = float64(bt.sumReqCost) * 2 / float64(dt)
-		}
-	}
-	return bt.balanceToPriority(bt.reducedBalance(at, avgReqCost))
-}
-
-// addBalance updates balance based on the time factor
-func (bt *balanceTracker) addBalance(now mclock.AbsTime) {
-	if now > bt.lastUpdate {
-		bt.balance = bt.reducedBalance(now, 0)
-		bt.lastUpdate = now
-	}
-}
-
-// checkCallbacks checks whether the threshold of any of the active callbacks
-// have been reached and calls them if necessary. It also sets up or updates
-// a scheduled event to ensure that is will be called again just after the next
-// threshold has been reached.
-// Note: checkCallbacks assumes that the balance has been recently updated.
-func (bt *balanceTracker) checkCallbacks(now mclock.AbsTime) {
-	if bt.callbackCount == 0 {
-		return
-	}
-	pri := bt.balanceToPriority(bt.balance)
-	for bt.callbackCount != 0 && bt.callbacks[bt.callbackCount-1].threshold <= pri {
-		bt.callbackCount--
-		bt.callbackIndex[bt.callbacks[bt.callbackCount].id] = -1
-		go bt.callbacks[bt.callbackCount].callback()
-	}
-	if bt.callbackCount != 0 {
-		d, ok := bt.timeUntil(bt.callbacks[bt.callbackCount-1].threshold)
-		if !ok {
-			bt.nextUpdate = 0
-			bt.updateAfter(0)
-			return
-		}
-		if bt.nextUpdate == 0 || bt.nextUpdate > now+mclock.AbsTime(d) {
-			if d > time.Second {
-				// Note: if the scheduled update is not in the very near future then we
-				// schedule the update a bit earlier. This way we do need to update a few
-				// extra times but don't need to reschedule every time a processed request
-				// brings the expected firing time a little bit closer.
-				d = ((d - time.Second) * 7 / 8) + time.Second
-			}
-			bt.nextUpdate = now + mclock.AbsTime(d)
-			bt.updateAfter(d)
-		}
-	} else {
-		bt.nextUpdate = 0
-		bt.updateAfter(0)
-	}
-}
-
-// updateAfter schedules a balance update and callback check in the future
-func (bt *balanceTracker) updateAfter(dt time.Duration) {
-	if bt.updateEvent == nil || bt.updateEvent.Stop() {
-		if dt == 0 {
-			bt.updateEvent = nil
-		} else {
-			bt.updateEvent = bt.clock.AfterFunc(dt, func() {
-				bt.lock.Lock()
-				defer bt.lock.Unlock()
-
-				if bt.callbackCount != 0 {
-					now := bt.clock.Now()
-					bt.addBalance(now)
-					bt.checkCallbacks(now)
-				}
-			})
-		}
-	}
-}
-
-// requestCost should be called after serving a request for the given peer
-func (bt *balanceTracker) requestCost(cost uint64) {
-	bt.lock.Lock()
-	defer bt.lock.Unlock()
-
-	if bt.stopped {
-		return
-	}
-	now := bt.clock.Now()
-	bt.addBalance(now)
-	fcost := float64(cost)
-
-	if bt.balance.pos != 0 {
-		if bt.requestFactor != 0 {
-			c := uint64(fcost * bt.requestFactor)
-			if bt.balance.pos >= c {
-				bt.balance.pos -= c
-				fcost = 0
-			} else {
-				fcost *= 1 - float64(bt.balance.pos)/float64(c)
-				bt.balance.pos = 0
-			}
-			bt.checkCallbacks(now)
-		} else {
-			fcost = 0
-		}
-	}
-	if fcost > 0 {
-		if bt.negRequestFactor != 0 {
-			bt.balance.neg += uint64(fcost * bt.negRequestFactor)
-			bt.checkCallbacks(now)
-		}
-	}
-	bt.sumReqCost += cost
-}
-
-// getBalance returns the current positive and negative balance
-func (bt *balanceTracker) getBalance(now mclock.AbsTime) (uint64, uint64) {
-	bt.lock.Lock()
-	defer bt.lock.Unlock()
-
-	bt.addBalance(now)
-	return bt.balance.pos, bt.balance.neg
-}
-
-// setBalance sets the positive and negative balance to the given values
-func (bt *balanceTracker) setBalance(pos, neg uint64) error {
-	bt.lock.Lock()
-	defer bt.lock.Unlock()
-
-	now := bt.clock.Now()
-	bt.addBalance(now)
-	bt.balance.pos = pos
-	bt.balance.neg = neg
-	bt.checkCallbacks(now)
-	return nil
-}
-
-// setFactors sets the price factors. timeFactor is the price of a nanosecond of
-// connection while requestFactor is the price of a "realCost" unit.
-func (bt *balanceTracker) setFactors(neg bool, timeFactor, requestFactor float64) {
-	bt.lock.Lock()
-	defer bt.lock.Unlock()
-
-	if bt.stopped {
-		return
-	}
-	now := bt.clock.Now()
-	bt.addBalance(now)
-	if neg {
-		bt.negTimeFactor = timeFactor
-		bt.negRequestFactor = requestFactor
-	} else {
-		bt.timeFactor = timeFactor
-		bt.requestFactor = requestFactor
-	}
-	bt.checkCallbacks(now)
-}
-
-// setCallback sets up a one-time callback to be called when priority reaches
-// the threshold. If it has already reached the threshold the callback is called
-// immediately.
-func (bt *balanceTracker) addCallback(id int, threshold int64, callback func()) {
-	bt.lock.Lock()
-	defer bt.lock.Unlock()
-
-	bt.removeCb(id)
-	idx := 0
-	for idx < bt.callbackCount && threshold < bt.callbacks[idx].threshold {
-		idx++
-	}
-	for i := bt.callbackCount - 1; i >= idx; i-- {
-		bt.callbackIndex[bt.callbacks[i].id]++
-		bt.callbacks[i+1] = bt.callbacks[i]
-	}
-	bt.callbackCount++
-	bt.callbackIndex[id] = idx
-	bt.callbacks[idx] = balanceCallback{id, threshold, callback}
-	now := bt.clock.Now()
-	bt.addBalance(now)
-	bt.checkCallbacks(now)
-}
-
-// removeCallback removes the given callback and returns true if it was active
-func (bt *balanceTracker) removeCallback(id int) bool {
-	bt.lock.Lock()
-	defer bt.lock.Unlock()
-
-	return bt.removeCb(id)
-}
-
-// removeCb removes the given callback and returns true if it was active
-// Note: should be called while bt.lock is held
-func (bt *balanceTracker) removeCb(id int) bool {
-	idx := bt.callbackIndex[id]
-	if idx == -1 {
-		return false
-	}
-	bt.callbackIndex[id] = -1
-	for i := idx; i < bt.callbackCount-1; i++ {
-		bt.callbackIndex[bt.callbacks[i+1].id]--
-		bt.callbacks[i] = bt.callbacks[i+1]
-	}
-	bt.callbackCount--
-	return true
-}
diff --git a/les/balance_test.go b/les/balance_test.go
deleted file mode 100644
index b571c2cc5c2de35d0ef0150d11e1c8e7f60441c9..0000000000000000000000000000000000000000
--- a/les/balance_test.go
+++ /dev/null
@@ -1,260 +0,0 @@
-// Copyright 2019 The go-ethereum Authors
-// This file is part of the go-ethereum library.
-//
-// The go-ethereum library is free software: you can redistribute it and/or modify
-// it under the terms of the GNU Lesser General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
-//
-// The go-ethereum library is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU Lesser General Public License for more details.
-//
-// You should have received a copy of the GNU Lesser General Public License
-// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
-
-package les
-
-import (
-	"testing"
-	"time"
-
-	"github.com/ethereum/go-ethereum/common/mclock"
-)
-
-func TestSetBalance(t *testing.T) {
-	var clock = &mclock.Simulated{}
-	var inputs = []struct {
-		pos uint64
-		neg uint64
-	}{
-		{1000, 0},
-		{0, 1000},
-		{1000, 1000},
-	}
-
-	tracker := balanceTracker{}
-	tracker.init(clock, 1000)
-	defer tracker.stop(clock.Now())
-
-	for _, i := range inputs {
-		tracker.setBalance(i.pos, i.neg)
-		pos, neg := tracker.getBalance(clock.Now())
-		if pos != i.pos {
-			t.Fatalf("Positive balance mismatch, want %v, got %v", i.pos, pos)
-		}
-		if neg != i.neg {
-			t.Fatalf("Negative balance mismatch, want %v, got %v", i.neg, neg)
-		}
-	}
-}
-
-func TestBalanceTimeCost(t *testing.T) {
-	var (
-		clock   = &mclock.Simulated{}
-		tracker = balanceTracker{}
-	)
-	tracker.init(clock, 1000)
-	defer tracker.stop(clock.Now())
-	tracker.setFactors(false, 1, 1)
-	tracker.setFactors(true, 1, 1)
-
-	tracker.setBalance(uint64(time.Minute), 0) // 1 minute time allowance
-
-	var inputs = []struct {
-		runTime time.Duration
-		expPos  uint64
-		expNeg  uint64
-	}{
-		{time.Second, uint64(time.Second * 59), 0},
-		{0, uint64(time.Second * 59), 0},
-		{time.Second * 59, 0, 0},
-		{time.Second, 0, uint64(time.Second)},
-	}
-	for _, i := range inputs {
-		clock.Run(i.runTime)
-		if pos, _ := tracker.getBalance(clock.Now()); pos != i.expPos {
-			t.Fatalf("Positive balance mismatch, want %v, got %v", i.expPos, pos)
-		}
-		if _, neg := tracker.getBalance(clock.Now()); neg != i.expNeg {
-			t.Fatalf("Negative balance mismatch, want %v, got %v", i.expNeg, neg)
-		}
-	}
-
-	tracker.setBalance(uint64(time.Minute), 0) // Refill 1 minute time allowance
-	for _, i := range inputs {
-		clock.Run(i.runTime)
-		if pos, _ := tracker.getBalance(clock.Now()); pos != i.expPos {
-			t.Fatalf("Positive balance mismatch, want %v, got %v", i.expPos, pos)
-		}
-		if _, neg := tracker.getBalance(clock.Now()); neg != i.expNeg {
-			t.Fatalf("Negative balance mismatch, want %v, got %v", i.expNeg, neg)
-		}
-	}
-}
-
-func TestBalanceReqCost(t *testing.T) {
-	var (
-		clock   = &mclock.Simulated{}
-		tracker = balanceTracker{}
-	)
-	tracker.init(clock, 1000)
-	defer tracker.stop(clock.Now())
-	tracker.setFactors(false, 1, 1)
-	tracker.setFactors(true, 1, 1)
-
-	tracker.setBalance(uint64(time.Minute), 0) // 1 minute time serving time allowance
-	var inputs = []struct {
-		reqCost uint64
-		expPos  uint64
-		expNeg  uint64
-	}{
-		{uint64(time.Second), uint64(time.Second * 59), 0},
-		{0, uint64(time.Second * 59), 0},
-		{uint64(time.Second * 59), 0, 0},
-		{uint64(time.Second), 0, uint64(time.Second)},
-	}
-	for _, i := range inputs {
-		tracker.requestCost(i.reqCost)
-		if pos, _ := tracker.getBalance(clock.Now()); pos != i.expPos {
-			t.Fatalf("Positive balance mismatch, want %v, got %v", i.expPos, pos)
-		}
-		if _, neg := tracker.getBalance(clock.Now()); neg != i.expNeg {
-			t.Fatalf("Negative balance mismatch, want %v, got %v", i.expNeg, neg)
-		}
-	}
-}
-
-func TestBalanceToPriority(t *testing.T) {
-	var (
-		clock   = &mclock.Simulated{}
-		tracker = balanceTracker{}
-	)
-	tracker.init(clock, 1000) // cap = 1000
-	defer tracker.stop(clock.Now())
-	tracker.setFactors(false, 1, 1)
-	tracker.setFactors(true, 1, 1)
-
-	var inputs = []struct {
-		pos      uint64
-		neg      uint64
-		priority int64
-	}{
-		{1000, 0, ^int64(1)},
-		{2000, 0, ^int64(2)}, // Higher balance, lower priority value
-		{0, 0, 0},
-		{0, 1000, 1000},
-	}
-	for _, i := range inputs {
-		tracker.setBalance(i.pos, i.neg)
-		priority := tracker.getPriority(clock.Now())
-		if priority != i.priority {
-			t.Fatalf("Priority mismatch, want %v, got %v", i.priority, priority)
-		}
-	}
-}
-
-func TestEstimatedPriority(t *testing.T) {
-	var (
-		clock   = &mclock.Simulated{}
-		tracker = balanceTracker{}
-	)
-	tracker.init(clock, 1000000000) // cap = 1000,000,000
-	defer tracker.stop(clock.Now())
-	tracker.setFactors(false, 1, 1)
-	tracker.setFactors(true, 1, 1)
-
-	tracker.setBalance(uint64(time.Minute), 0)
-	var inputs = []struct {
-		runTime    time.Duration // time cost
-		futureTime time.Duration // diff of future time
-		reqCost    uint64        // single request cost
-		priority   int64         // expected estimated priority
-	}{
-		{time.Second, time.Second, 0, ^int64(58)},
-		{0, time.Second, 0, ^int64(58)},
-
-		// 2 seconds time cost, 1 second estimated time cost, 10^9 request cost,
-		// 10^9 estimated request cost per second.
-		{time.Second, time.Second, 1000000000, ^int64(55)},
-
-		// 3 seconds time cost, 3 second estimated time cost, 10^9*2 request cost,
-		// 4*10^9 estimated request cost.
-		{time.Second, 3 * time.Second, 1000000000, ^int64(48)},
-
-		// All positive balance is used up
-		{time.Second * 55, 0, 0, 0},
-
-		// 1 minute estimated time cost, 4/58 * 10^9 estimated request cost per sec.
-		{0, time.Minute, 0, int64(time.Minute) + int64(time.Second)*120/29},
-	}
-	for _, i := range inputs {
-		clock.Run(i.runTime)
-		tracker.requestCost(i.reqCost)
-		priority := tracker.estimatedPriority(clock.Now()+mclock.AbsTime(i.futureTime), true)
-		if priority != i.priority {
-			t.Fatalf("Estimated priority mismatch, want %v, got %v", i.priority, priority)
-		}
-	}
-}
-
-func TestCallbackChecking(t *testing.T) {
-	var (
-		clock   = &mclock.Simulated{}
-		tracker = balanceTracker{}
-	)
-	tracker.init(clock, 1000000) // cap = 1000,000
-	defer tracker.stop(clock.Now())
-	tracker.setFactors(false, 1, 1)
-	tracker.setFactors(true, 1, 1)
-
-	var inputs = []struct {
-		priority int64
-		expDiff  time.Duration
-	}{
-		{^int64(500), time.Millisecond * 500},
-		{0, time.Second},
-		{int64(time.Second), 2 * time.Second},
-	}
-	tracker.setBalance(uint64(time.Second), 0)
-	for _, i := range inputs {
-		diff, _ := tracker.timeUntil(i.priority)
-		if diff != i.expDiff {
-			t.Fatalf("Time difference mismatch, want %v, got %v", i.expDiff, diff)
-		}
-	}
-}
-
-func TestCallback(t *testing.T) {
-	var (
-		clock   = &mclock.Simulated{}
-		tracker = balanceTracker{}
-	)
-	tracker.init(clock, 1000) // cap = 1000
-	defer tracker.stop(clock.Now())
-	tracker.setFactors(false, 1, 1)
-	tracker.setFactors(true, 1, 1)
-
-	callCh := make(chan struct{}, 1)
-	tracker.setBalance(uint64(time.Minute), 0)
-	tracker.addCallback(balanceCallbackZero, 0, func() { callCh <- struct{}{} })
-
-	clock.Run(time.Minute)
-	select {
-	case <-callCh:
-	case <-time.NewTimer(time.Second).C:
-		t.Fatalf("Callback hasn't been called yet")
-	}
-
-	tracker.setBalance(uint64(time.Minute), 0)
-	tracker.addCallback(balanceCallbackZero, 0, func() { callCh <- struct{}{} })
-	tracker.removeCallback(balanceCallbackZero)
-
-	clock.Run(time.Minute)
-	select {
-	case <-callCh:
-		t.Fatalf("Callback shouldn't be called")
-	case <-time.NewTimer(time.Millisecond * 100).C:
-	}
-}
diff --git a/les/clientpool.go b/les/clientpool.go
index 9c4060fc2c599c0f2045d998995b613929227031..4f6e3fafe01d2cf0f1174b7f2b52c8ebf712cb11 100644
--- a/les/clientpool.go
+++ b/les/clientpool.go
@@ -17,34 +17,48 @@
 package les
 
 import (
-	"bytes"
-	"encoding/binary"
 	"fmt"
-	"io"
-	"math"
+	"reflect"
 	"sync"
 	"time"
 
-	"github.com/ethereum/go-ethereum/common"
 	"github.com/ethereum/go-ethereum/common/mclock"
-	"github.com/ethereum/go-ethereum/common/prque"
 	"github.com/ethereum/go-ethereum/ethdb"
+	lps "github.com/ethereum/go-ethereum/les/lespay/server"
+	"github.com/ethereum/go-ethereum/les/utils"
 	"github.com/ethereum/go-ethereum/log"
 	"github.com/ethereum/go-ethereum/p2p/enode"
-	"github.com/ethereum/go-ethereum/rlp"
-	lru "github.com/hashicorp/golang-lru"
+	"github.com/ethereum/go-ethereum/p2p/enr"
+	"github.com/ethereum/go-ethereum/p2p/nodestate"
 )
 
 const (
-	negBalanceExpTC              = time.Hour        // time constant for exponentially reducing negative balance
-	fixedPointMultiplier         = 0x1000000        // constant to convert logarithms to fixed point format
-	lazyQueueRefresh             = time.Second * 10 // refresh period of the connected queue
-	persistCumulativeTimeRefresh = time.Minute * 5  // refresh period of the cumulative running time persistence
-	posBalanceCacheLimit         = 8192             // the maximum number of cached items in positive balance queue
-	negBalanceCacheLimit         = 8192             // the maximum number of cached items in negative balance queue
-	defaultConnectedBias         = time.Minute * 3  // the default connectedBias used in clientPool
+	defaultNegExpTC = 3600 // default time constant (in seconds) for exponentially reducing negative balance
+
+	// defaultConnectedBias is applied to already connected clients So that
+	// already connected client won't be kicked out very soon and we
+	// can ensure all connected clients can have enough time to request
+	// or sync some data.
+	//
+	// todo(rjl493456442) make it configurable. It can be the option of
+	// free trial time!
+	defaultConnectedBias = time.Minute * 3
+	inactiveTimeout      = time.Second * 10
+)
+
+var (
+	clientPoolSetup     = &nodestate.Setup{}
+	clientField         = clientPoolSetup.NewField("clientInfo", reflect.TypeOf(&clientInfo{}))
+	connAddressField    = clientPoolSetup.NewField("connAddr", reflect.TypeOf(""))
+	balanceTrackerSetup = lps.NewBalanceTrackerSetup(clientPoolSetup)
+	priorityPoolSetup   = lps.NewPriorityPoolSetup(clientPoolSetup)
 )
 
+func init() {
+	balanceTrackerSetup.Connect(connAddressField, priorityPoolSetup.CapacityField)
+	priorityPoolSetup.Connect(balanceTrackerSetup.BalanceField, balanceTrackerSetup.UpdateFlag) // NodeBalance implements nodePriority
+}
+
 // clientPool implements a client database that assigns a priority to each client
 // based on a positive and negative balance. Positive balance is externally assigned
 // to prioritized clients and is decreased with connection time and processed
@@ -52,7 +66,7 @@ const (
 // then negative balance is accumulated.
 //
 // Balance tracking and priority calculation for connected clients is done by
-// balanceTracker. connectedQueue ensures that clients with the lowest positive or
+// balanceTracker. activeQueue ensures that clients with the lowest positive or
 // highest negative balance get evicted when the total capacity allowance is full
 // and new clients with a better balance want to connect.
 //
@@ -61,32 +75,24 @@ const (
 // each client can have several minutes of connection time.
 //
 // Balances of disconnected clients are stored in nodeDB including positive balance
-// and negative banalce. Negative balance is transformed into a logarithmic form
-// with a constantly shifting linear offset in order to implement an exponential
-// decrease. Besides nodeDB will have a background thread to check the negative
-// balance of disconnected client. If the balance is low enough, then the record
-// will be dropped.
+// and negative banalce. Boeth positive balance and negative balance will decrease
+// exponentially. If the balance is low enough, then the record will be dropped.
 type clientPool struct {
-	ndb        *nodeDB
+	lps.BalanceTrackerSetup
+	lps.PriorityPoolSetup
 	lock       sync.Mutex
 	clock      mclock.Clock
-	stopCh     chan struct{}
 	closed     bool
 	removePeer func(enode.ID)
+	ns         *nodestate.NodeStateMachine
+	pp         *lps.PriorityPool
+	bt         *lps.BalanceTracker
 
-	connectedMap   map[enode.ID]*clientInfo
-	connectedQueue *prque.LazyQueue
-
-	defaultPosFactors, defaultNegFactors priceFactors
-
-	connLimit         int            // The maximum number of connections that clientpool can support
-	capLimit          uint64         // The maximum cumulative capacity that clientpool can support
-	connectedCap      uint64         // The sum of the capacity of the current clientpool connected
-	priorityConnected uint64         // The sum of the capacity of currently connected priority clients
-	freeClientCap     uint64         // The capacity value of each free client
-	startTime         mclock.AbsTime // The timestamp at which the clientpool started running
-	cumulativeTime    int64          // The cumulative running time of clientpool at the start point.
-	connectedBias     time.Duration  // The connection bias. 0: Disable connection bias(used in testing)
+	defaultPosFactors, defaultNegFactors lps.PriceFactors
+	posExpTC, negExpTC                   uint64
+	minCap                               uint64 // The minimal capacity value allowed for any client
+	connectedBias                        time.Duration
+	capLimit                             uint64
 }
 
 // clientPoolPeer represents a client peer in the pool.
@@ -95,264 +101,190 @@ type clientPool struct {
 // clients have a limited access to IP addresses while new node keys can be easily
 // generated so it would be useless to assign a negative value to them.
 type clientPoolPeer interface {
-	ID() enode.ID
+	Node() *enode.Node
 	freeClientId() string
 	updateCapacity(uint64)
-	freezeClient()
+	freeze()
+	allowInactive() bool
 }
 
-// clientInfo represents a connected client
+// clientInfo defines all information required by clientpool.
 type clientInfo struct {
-	address                string
-	id                     enode.ID
-	connectedAt            mclock.AbsTime
-	capacity               uint64
-	priority               bool
-	pool                   *clientPool
-	peer                   clientPoolPeer
-	queueIndex             int // position in connectedQueue
-	balanceTracker         balanceTracker
-	posFactors, negFactors priceFactors
-	balanceMetaInfo        string
-}
-
-// connSetIndex callback updates clientInfo item index in connectedQueue
-func connSetIndex(a interface{}, index int) {
-	a.(*clientInfo).queueIndex = index
+	node                *enode.Node
+	address             string
+	peer                clientPoolPeer
+	connected, priority bool
+	connectedAt         mclock.AbsTime
+	balance             *lps.NodeBalance
 }
 
-// connPriority callback returns actual priority of clientInfo item in connectedQueue
-func connPriority(a interface{}, now mclock.AbsTime) int64 {
-	c := a.(*clientInfo)
-	return c.balanceTracker.getPriority(now)
-}
-
-// connMaxPriority callback returns estimated maximum priority of clientInfo item in connectedQueue
-func connMaxPriority(a interface{}, until mclock.AbsTime) int64 {
-	c := a.(*clientInfo)
-	pri := c.balanceTracker.estimatedPriority(until, true)
-	c.balanceTracker.addCallback(balanceCallbackQueue, pri+1, func() {
-		c.pool.lock.Lock()
-		if c.queueIndex != -1 {
-			c.pool.connectedQueue.Update(c.queueIndex)
+// newClientPool creates a new client pool
+func newClientPool(lespayDb ethdb.Database, minCap uint64, connectedBias time.Duration, clock mclock.Clock, removePeer func(enode.ID)) *clientPool {
+	ns := nodestate.NewNodeStateMachine(nil, nil, clock, clientPoolSetup)
+	pool := &clientPool{
+		ns:                  ns,
+		BalanceTrackerSetup: balanceTrackerSetup,
+		PriorityPoolSetup:   priorityPoolSetup,
+		clock:               clock,
+		minCap:              minCap,
+		connectedBias:       connectedBias,
+		removePeer:          removePeer,
+	}
+	pool.bt = lps.NewBalanceTracker(ns, balanceTrackerSetup, lespayDb, clock, &utils.Expirer{}, &utils.Expirer{})
+	pool.pp = lps.NewPriorityPool(ns, priorityPoolSetup, clock, minCap, connectedBias, 4)
+
+	// set default expiration constants used by tests
+	// Note: server overwrites this if token sale is active
+	pool.bt.SetExpirationTCs(0, defaultNegExpTC)
+
+	ns.SubscribeState(pool.InactiveFlag.Or(pool.PriorityFlag), func(node *enode.Node, oldState, newState nodestate.Flags) {
+		if newState.Equals(pool.InactiveFlag) {
+			ns.AddTimeout(node, pool.InactiveFlag, inactiveTimeout)
+		}
+		if oldState.Equals(pool.InactiveFlag) && newState.Equals(pool.InactiveFlag.Or(pool.PriorityFlag)) {
+			ns.SetStateSub(node, pool.InactiveFlag, nodestate.Flags{}, 0) // remove timeout
 		}
-		c.pool.lock.Unlock()
 	})
-	return pri
-}
 
-// priceFactors determine the pricing policy (may apply either to positive or
-// negative balances which may have different factors).
-// - timeFactor is cost unit per nanosecond of connection time
-// - capacityFactor is cost unit per nanosecond of connection time per 1000000 capacity
-// - requestFactor is cost unit per request "realCost" unit
-type priceFactors struct {
-	timeFactor, capacityFactor, requestFactor float64
-}
+	ns.SubscribeState(pool.ActiveFlag.Or(pool.PriorityFlag), func(node *enode.Node, oldState, newState nodestate.Flags) {
+		c, _ := ns.GetField(node, clientField).(*clientInfo)
+		if c == nil {
+			return
+		}
+		c.priority = newState.HasAll(pool.PriorityFlag)
+		if newState.Equals(pool.ActiveFlag) {
+			cap, _ := ns.GetField(node, pool.CapacityField).(uint64)
+			if cap > minCap {
+				pool.pp.RequestCapacity(node, minCap, 0, true)
+			}
+		}
+	})
 
-// newClientPool creates a new client pool
-func newClientPool(db ethdb.Database, freeClientCap uint64, clock mclock.Clock, removePeer func(enode.ID)) *clientPool {
-	ndb := newNodeDB(db, clock)
-	pool := &clientPool{
-		ndb:            ndb,
-		clock:          clock,
-		connectedMap:   make(map[enode.ID]*clientInfo),
-		connectedQueue: prque.NewLazyQueue(connSetIndex, connPriority, connMaxPriority, clock, lazyQueueRefresh),
-		freeClientCap:  freeClientCap,
-		removePeer:     removePeer,
-		startTime:      clock.Now(),
-		cumulativeTime: ndb.getCumulativeTime(),
-		stopCh:         make(chan struct{}),
-		connectedBias:  defaultConnectedBias,
-	}
-	// If the negative balance of free client is even lower than 1,
-	// delete this entry.
-	ndb.nbEvictCallBack = func(now mclock.AbsTime, b negBalance) bool {
-		balance := math.Exp(float64(b.logValue-pool.logOffset(now)) / fixedPointMultiplier)
-		return balance <= 1
-	}
-	go func() {
-		for {
-			select {
-			case <-clock.After(lazyQueueRefresh):
-				pool.lock.Lock()
-				pool.connectedQueue.Refresh()
-				pool.lock.Unlock()
-			case <-clock.After(persistCumulativeTimeRefresh):
-				pool.ndb.setCumulativeTime(pool.logOffset(clock.Now()))
-			case <-pool.stopCh:
-				return
+	ns.SubscribeState(pool.InactiveFlag.Or(pool.ActiveFlag), func(node *enode.Node, oldState, newState nodestate.Flags) {
+		if oldState.IsEmpty() {
+			clientConnectedMeter.Mark(1)
+			log.Debug("Client connected", "id", node.ID())
+		}
+		if oldState.Equals(pool.InactiveFlag) && newState.Equals(pool.ActiveFlag) {
+			clientActivatedMeter.Mark(1)
+			log.Debug("Client activated", "id", node.ID())
+		}
+		if oldState.Equals(pool.ActiveFlag) && newState.Equals(pool.InactiveFlag) {
+			clientDeactivatedMeter.Mark(1)
+			log.Debug("Client deactivated", "id", node.ID())
+			c, _ := ns.GetField(node, clientField).(*clientInfo)
+			if c == nil || !c.peer.allowInactive() {
+				pool.removePeer(node.ID())
 			}
 		}
-	}()
+		if newState.IsEmpty() {
+			clientDisconnectedMeter.Mark(1)
+			log.Debug("Client disconnected", "id", node.ID())
+			pool.removePeer(node.ID())
+		}
+	})
+
+	var totalConnected uint64
+	ns.SubscribeField(pool.CapacityField, func(node *enode.Node, state nodestate.Flags, oldValue, newValue interface{}) {
+		oldCap, _ := oldValue.(uint64)
+		newCap, _ := newValue.(uint64)
+		totalConnected += newCap - oldCap
+		totalConnectedGauge.Update(int64(totalConnected))
+		c, _ := ns.GetField(node, clientField).(*clientInfo)
+		if c != nil {
+			c.peer.updateCapacity(newCap)
+		}
+	})
+
+	ns.Start()
 	return pool
 }
 
 // stop shuts the client pool down
 func (f *clientPool) stop() {
-	close(f.stopCh)
 	f.lock.Lock()
 	f.closed = true
 	f.lock.Unlock()
-	f.ndb.setCumulativeTime(f.logOffset(f.clock.Now()))
-	f.ndb.close()
+	f.ns.ForEach(nodestate.Flags{}, nodestate.Flags{}, func(node *enode.Node, state nodestate.Flags) {
+		// enforces saving all balances in BalanceTracker
+		f.disconnectNode(node)
+	})
+	f.bt.Stop()
+	f.ns.Stop()
 }
 
 // connect should be called after a successful handshake. If the connection was
 // rejected, there is no need to call disconnect.
-func (f *clientPool) connect(peer clientPoolPeer, capacity uint64) bool {
+func (f *clientPool) connect(peer clientPoolPeer) (uint64, error) {
 	f.lock.Lock()
 	defer f.lock.Unlock()
 
 	// Short circuit if clientPool is already closed.
 	if f.closed {
-		return false
+		return 0, fmt.Errorf("Client pool is already closed")
 	}
 	// Dedup connected peers.
-	id, freeID := peer.ID(), peer.freeClientId()
-	if _, ok := f.connectedMap[id]; ok {
-		clientRejectedMeter.Mark(1)
-		log.Debug("Client already connected", "address", freeID, "id", id.String())
-		return false
-	}
-	// Create a clientInfo but do not add it yet
-	var (
-		posBalance uint64
-		negBalance uint64
-		now        = f.clock.Now()
-	)
-	pb := f.ndb.getOrNewPB(id)
-	posBalance = pb.value
-
-	nb := f.ndb.getOrNewNB(freeID)
-	if nb.logValue != 0 {
-		negBalance = uint64(math.Exp(float64(nb.logValue-f.logOffset(now))/fixedPointMultiplier) * float64(time.Second))
-	}
-	e := &clientInfo{
-		pool:            f,
-		peer:            peer,
-		address:         freeID,
-		queueIndex:      -1,
-		id:              id,
-		connectedAt:     now,
-		priority:        posBalance != 0,
-		posFactors:      f.defaultPosFactors,
-		negFactors:      f.defaultNegFactors,
-		balanceMetaInfo: pb.meta,
-	}
-	// If the client is a free client, assign with a low free capacity,
-	// Otherwise assign with the given value(priority client)
-	if !e.priority || capacity == 0 {
-		capacity = f.freeClientCap
+	node, freeID := peer.Node(), peer.freeClientId()
+	if f.ns.GetField(node, clientField) != nil {
+		log.Debug("Client already connected", "address", freeID, "id", node.ID().String())
+		return 0, fmt.Errorf("Client already connected address=%s id=%s", freeID, node.ID().String())
+	}
+	now := f.clock.Now()
+	c := &clientInfo{
+		node:        node,
+		address:     freeID,
+		peer:        peer,
+		connected:   true,
+		connectedAt: now,
+	}
+	f.ns.SetField(node, clientField, c)
+	f.ns.SetField(node, connAddressField, freeID)
+	if c.balance, _ = f.ns.GetField(node, f.BalanceField).(*lps.NodeBalance); c.balance == nil {
+		f.disconnect(peer)
+		return 0, nil
+	}
+	c.balance.SetPriceFactors(f.defaultPosFactors, f.defaultNegFactors)
+
+	f.ns.SetState(node, f.InactiveFlag, nodestate.Flags{}, 0)
+	var allowed bool
+	f.ns.Operation(func() {
+		_, allowed = f.pp.RequestCapacity(node, f.minCap, f.connectedBias, true)
+	})
+	if allowed {
+		return f.minCap, nil
 	}
-	e.capacity = capacity
-
-	// Starts a balance tracker
-	e.balanceTracker.init(f.clock, capacity)
-	e.balanceTracker.setBalance(posBalance, negBalance)
-	e.updatePriceFactors()
-
-	// If the number of clients already connected in the clientpool exceeds its
-	// capacity, evict some clients with lowest priority.
-	//
-	// If the priority of the newly added client is lower than the priority of
-	// all connected clients, the client is rejected.
-	newCapacity := f.connectedCap + capacity
-	newCount := f.connectedQueue.Size() + 1
-	if newCapacity > f.capLimit || newCount > f.connLimit {
-		var (
-			kickList     []*clientInfo
-			kickPriority int64
-		)
-		f.connectedQueue.MultiPop(func(data interface{}, priority int64) bool {
-			c := data.(*clientInfo)
-			kickList = append(kickList, c)
-			kickPriority = priority
-			newCapacity -= c.capacity
-			newCount--
-			return newCapacity > f.capLimit || newCount > f.connLimit
-		})
-		if newCapacity > f.capLimit || newCount > f.connLimit || (e.balanceTracker.estimatedPriority(now+mclock.AbsTime(f.connectedBias), false)-kickPriority) > 0 {
-			for _, c := range kickList {
-				f.connectedQueue.Push(c)
-			}
-			clientRejectedMeter.Mark(1)
-			log.Debug("Client rejected", "address", freeID, "id", id.String())
-			return false
-		}
-		// accept new client, drop old ones
-		for _, c := range kickList {
-			f.dropClient(c, now, true)
-		}
+	if !peer.allowInactive() {
+		f.disconnect(peer)
 	}
+	return 0, nil
+}
 
-	// Register new client to connection queue.
-	f.connectedMap[id] = e
-	f.connectedQueue.Push(e)
-	f.connectedCap += e.capacity
+// setConnectedBias sets the connection bias, which is applied to already connected clients
+// So that already connected client won't be kicked out very soon and we can ensure all
+// connected clients can have enough time to request or sync some data.
+func (f *clientPool) setConnectedBias(bias time.Duration) {
+	f.lock.Lock()
+	defer f.lock.Unlock()
 
-	// If the current client is a paid client, monitor the status of client,
-	// downgrade it to normal client if positive balance is used up.
-	if e.priority {
-		f.priorityConnected += capacity
-		e.balanceTracker.addCallback(balanceCallbackZero, 0, func() { f.balanceExhausted(id) })
-	}
-	// If the capacity of client is not the default value(free capacity), notify
-	// it to update capacity.
-	if e.capacity != f.freeClientCap {
-		e.peer.updateCapacity(e.capacity)
-	}
-	totalConnectedGauge.Update(int64(f.connectedCap))
-	clientConnectedMeter.Mark(1)
-	log.Debug("Client accepted", "address", freeID)
-	return true
+	f.connectedBias = bias
+	f.pp.SetActiveBias(bias)
 }
 
 // disconnect should be called when a connection is terminated. If the disconnection
 // was initiated by the pool itself using disconnectFn then calling disconnect is
 // not necessary but permitted.
 func (f *clientPool) disconnect(p clientPoolPeer) {
-	f.lock.Lock()
-	defer f.lock.Unlock()
-
-	// Short circuit if client pool is already closed.
-	if f.closed {
-		return
-	}
-	// Short circuit if the peer hasn't been registered.
-	e := f.connectedMap[p.ID()]
-	if e == nil {
-		log.Debug("Client not connected", "address", p.freeClientId(), "id", p.ID().String())
-		return
-	}
-	f.dropClient(e, f.clock.Now(), false)
+	f.disconnectNode(p.Node())
 }
 
-// forClients iterates through a list of clients, calling the callback for each one.
-// If a client is not connected then clientInfo is nil. If the specified list is empty
-// then the callback is called for all connected clients.
-func (f *clientPool) forClients(ids []enode.ID, callback func(*clientInfo, enode.ID) error) error {
-	f.lock.Lock()
-	defer f.lock.Unlock()
-
-	if len(ids) > 0 {
-		for _, id := range ids {
-			if err := callback(f.connectedMap[id], id); err != nil {
-				return err
-			}
-		}
-	} else {
-		for _, c := range f.connectedMap {
-			if err := callback(c, c.id); err != nil {
-				return err
-			}
-		}
-	}
-	return nil
+// disconnectNode removes node fields and flags related to connected status
+func (f *clientPool) disconnectNode(node *enode.Node) {
+	f.ns.SetField(node, connAddressField, nil)
+	f.ns.SetField(node, clientField, nil)
 }
 
 // setDefaultFactors sets the default price factors applied to subsequently connected clients
-func (f *clientPool) setDefaultFactors(posFactors, negFactors priceFactors) {
+func (f *clientPool) setDefaultFactors(posFactors, negFactors lps.PriceFactors) {
 	f.lock.Lock()
 	defer f.lock.Unlock()
 
@@ -360,512 +292,110 @@ func (f *clientPool) setDefaultFactors(posFactors, negFactors priceFactors) {
 	f.defaultNegFactors = negFactors
 }
 
-// setConnectedBias sets the connection bias, which is applied to already connected clients
-// So that already connected client won't be kicked out very soon and we can ensure all
-// connected clients can have enough time to request or sync some data.
-func (f *clientPool) setConnectedBias(bias time.Duration) {
-	f.lock.Lock()
-	defer f.lock.Unlock()
-
-	f.connectedBias = bias
-}
-
-// dropClient removes a client from the connected queue and finalizes its balance.
-// If kick is true then it also initiates the disconnection.
-func (f *clientPool) dropClient(e *clientInfo, now mclock.AbsTime, kick bool) {
-	if _, ok := f.connectedMap[e.id]; !ok {
-		return
-	}
-	f.finalizeBalance(e, now)
-	f.connectedQueue.Remove(e.queueIndex)
-	delete(f.connectedMap, e.id)
-	f.connectedCap -= e.capacity
-	if e.priority {
-		f.priorityConnected -= e.capacity
-	}
-	totalConnectedGauge.Update(int64(f.connectedCap))
-	if kick {
-		clientKickedMeter.Mark(1)
-		log.Debug("Client kicked out", "address", e.address)
-		f.removePeer(e.id)
-	} else {
-		clientDisconnectedMeter.Mark(1)
-		log.Debug("Client disconnected", "address", e.address)
-	}
-}
-
 // capacityInfo returns the total capacity allowance, the total capacity of connected
 // clients and the total capacity of connected and prioritized clients
 func (f *clientPool) capacityInfo() (uint64, uint64, uint64) {
 	f.lock.Lock()
 	defer f.lock.Unlock()
 
-	return f.capLimit, f.connectedCap, f.priorityConnected
-}
-
-// finalizeBalance stops the balance tracker, retrieves the final balances and
-// stores them in posBalanceQueue and negBalanceQueue
-func (f *clientPool) finalizeBalance(c *clientInfo, now mclock.AbsTime) {
-	c.balanceTracker.stop(now)
-	pos, neg := c.balanceTracker.getBalance(now)
-
-	pb, nb := f.ndb.getOrNewPB(c.id), f.ndb.getOrNewNB(c.address)
-	pb.value = pos
-	f.ndb.setPB(c.id, pb)
-
-	neg /= uint64(time.Second) // Convert the expanse to second level.
-	if neg > 1 {
-		nb.logValue = int64(math.Log(float64(neg))*fixedPointMultiplier) + f.logOffset(now)
-		f.ndb.setNB(c.address, nb)
-	} else {
-		f.ndb.delNB(c.address) // Negative balance is small enough, drop it directly.
-	}
-}
-
-// balanceExhausted callback is called by balanceTracker when positive balance is exhausted.
-// It revokes priority status and also reduces the client capacity if necessary.
-func (f *clientPool) balanceExhausted(id enode.ID) {
-	f.lock.Lock()
-	defer f.lock.Unlock()
-
-	c := f.connectedMap[id]
-	if c == nil || !c.priority {
-		return
-	}
-	if c.priority {
-		f.priorityConnected -= c.capacity
-	}
-	c.priority = false
-	if c.capacity != f.freeClientCap {
-		f.connectedCap += f.freeClientCap - c.capacity
-		totalConnectedGauge.Update(int64(f.connectedCap))
-		c.capacity = f.freeClientCap
-		c.balanceTracker.setCapacity(c.capacity)
-		c.peer.updateCapacity(c.capacity)
-	}
-	pb := f.ndb.getOrNewPB(id)
-	pb.value = 0
-	f.ndb.setPB(id, pb)
+	// total priority active cap will be supported when the token issuer module is added
+	return f.capLimit, f.pp.ActiveCapacity(), 0
 }
 
-// setConnLimit sets the maximum number and total capacity of connected clients,
+// setLimits sets the maximum number and total capacity of connected clients,
 // dropping some of them if necessary.
 func (f *clientPool) setLimits(totalConn int, totalCap uint64) {
 	f.lock.Lock()
 	defer f.lock.Unlock()
 
-	f.connLimit = totalConn
 	f.capLimit = totalCap
-	if f.connectedCap > f.capLimit || f.connectedQueue.Size() > f.connLimit {
-		f.connectedQueue.MultiPop(func(data interface{}, priority int64) bool {
-			f.dropClient(data.(*clientInfo), mclock.Now(), true)
-			return f.connectedCap > f.capLimit || f.connectedQueue.Size() > f.connLimit
-		})
-	}
+	f.pp.SetLimits(uint64(totalConn), totalCap)
 }
 
 // setCapacity sets the assigned capacity of a connected client
-func (f *clientPool) setCapacity(c *clientInfo, capacity uint64) error {
-	if f.connectedMap[c.id] != c {
-		return fmt.Errorf("client %064x is not connected", c.id[:])
-	}
-	if c.capacity == capacity {
-		return nil
-	}
-	if !c.priority {
-		return errNoPriority
+func (f *clientPool) setCapacity(node *enode.Node, freeID string, capacity uint64, bias time.Duration, setCap bool) (uint64, error) {
+	c, _ := f.ns.GetField(node, clientField).(*clientInfo)
+	if c == nil {
+		if setCap {
+			return 0, fmt.Errorf("client %064x is not connected", node.ID())
+		}
+		c = &clientInfo{node: node}
+		f.ns.SetField(node, clientField, c)
+		f.ns.SetField(node, connAddressField, freeID)
+		if c.balance, _ = f.ns.GetField(node, f.BalanceField).(*lps.NodeBalance); c.balance == nil {
+			log.Error("BalanceField is missing", "node", node.ID())
+			return 0, fmt.Errorf("BalanceField of %064x is missing", node.ID())
+		}
+		defer func() {
+			f.ns.SetField(node, connAddressField, nil)
+			f.ns.SetField(node, clientField, nil)
+		}()
 	}
-	oldCapacity := c.capacity
-	c.capacity = capacity
-	f.connectedCap += capacity - oldCapacity
-	c.balanceTracker.setCapacity(capacity)
-	f.connectedQueue.Update(c.queueIndex)
-	if f.connectedCap > f.capLimit {
-		var kickList []*clientInfo
-		kick := true
-		f.connectedQueue.MultiPop(func(data interface{}, priority int64) bool {
-			client := data.(*clientInfo)
-			kickList = append(kickList, client)
-			f.connectedCap -= client.capacity
-			if client == c {
-				kick = false
-			}
-			return kick && (f.connectedCap > f.capLimit)
-		})
-		if kick {
-			now := mclock.Now()
-			for _, c := range kickList {
-				f.dropClient(c, now, true)
-			}
-		} else {
-			c.capacity = oldCapacity
-			c.balanceTracker.setCapacity(oldCapacity)
-			for _, c := range kickList {
-				f.connectedCap += c.capacity
-				f.connectedQueue.Push(c)
-			}
-			return errNoPriority
+	var (
+		minPriority int64
+		allowed     bool
+	)
+	f.ns.Operation(func() {
+		if !setCap || c.priority {
+			// check clientInfo.priority inside Operation to ensure thread safety
+			minPriority, allowed = f.pp.RequestCapacity(node, capacity, bias, setCap)
 		}
+	})
+	if allowed {
+		return 0, nil
 	}
-	totalConnectedGauge.Update(int64(f.connectedCap))
-	f.priorityConnected += capacity - oldCapacity
-	c.updatePriceFactors()
-	c.peer.updateCapacity(c.capacity)
-	return nil
-}
-
-// requestCost feeds request cost after serving a request from the given peer.
-func (f *clientPool) requestCost(p *clientPeer, cost uint64) {
-	f.lock.Lock()
-	defer f.lock.Unlock()
-
-	info, exist := f.connectedMap[p.ID()]
-	if !exist || f.closed {
-		return
+	missing := c.balance.PosBalanceMissing(minPriority, capacity, bias)
+	if missing < 1 {
+		// ensure that we never return 0 missing and insufficient priority error
+		missing = 1
 	}
-	info.balanceTracker.requestCost(cost)
+	return missing, errNoPriority
 }
 
-// logOffset calculates the time-dependent offset for the logarithmic
-// representation of negative balance
-//
-// From another point of view, the result returned by the function represents
-// the total time that the clientpool is cumulatively running(total_hours/multiplier).
-func (f *clientPool) logOffset(now mclock.AbsTime) int64 {
-	// Note: fixedPointMultiplier acts as a multiplier here; the reason for dividing the divisor
-	// is to avoid int64 overflow. We assume that int64(negBalanceExpTC) >> fixedPointMultiplier.
-	cumulativeTime := int64((time.Duration(now - f.startTime)) / (negBalanceExpTC / fixedPointMultiplier))
-	return f.cumulativeTime + cumulativeTime
-}
-
-// setClientPriceFactors sets the pricing factors for an individual connected client
-func (c *clientInfo) updatePriceFactors() {
-	c.balanceTracker.setFactors(true, c.negFactors.timeFactor+float64(c.capacity)*c.negFactors.capacityFactor/1000000, c.negFactors.requestFactor)
-	c.balanceTracker.setFactors(false, c.posFactors.timeFactor+float64(c.capacity)*c.posFactors.capacityFactor/1000000, c.posFactors.requestFactor)
-}
-
-// getPosBalance retrieves a single positive balance entry from cache or the database
-func (f *clientPool) getPosBalance(id enode.ID) posBalance {
+// setCapacityLocked is the equivalent of setCapacity used when f.lock is already locked
+func (f *clientPool) setCapacityLocked(node *enode.Node, freeID string, capacity uint64, minConnTime time.Duration, setCap bool) (uint64, error) {
 	f.lock.Lock()
 	defer f.lock.Unlock()
 
-	return f.ndb.getOrNewPB(id)
+	return f.setCapacity(node, freeID, capacity, minConnTime, setCap)
 }
 
-// addBalance updates the balance of a client (either overwrites it or adds to it).
-// It also updates the balance meta info string.
-func (f *clientPool) addBalance(id enode.ID, amount int64, meta string) (uint64, uint64, error) {
+// forClients calls the supplied callback for either the listed node IDs or all connected
+// nodes. It passes a valid clientInfo to the callback and ensures that the necessary
+// fields and flags are set in order for BalanceTracker and PriorityPool to work even if
+// the node is not connected.
+func (f *clientPool) forClients(ids []enode.ID, cb func(client *clientInfo)) {
 	f.lock.Lock()
 	defer f.lock.Unlock()
 
-	pb := f.ndb.getOrNewPB(id)
-	var negBalance uint64
-	c := f.connectedMap[id]
-	if c != nil {
-		pb.value, negBalance = c.balanceTracker.getBalance(f.clock.Now())
-	}
-	oldBalance := pb.value
-	if amount > 0 {
-		if amount > maxBalance || pb.value > maxBalance-uint64(amount) {
-			return oldBalance, oldBalance, errBalanceOverflow
-		}
-		pb.value += uint64(amount)
+	if len(ids) == 0 {
+		f.ns.ForEach(nodestate.Flags{}, nodestate.Flags{}, func(node *enode.Node, state nodestate.Flags) {
+			c, _ := f.ns.GetField(node, clientField).(*clientInfo)
+			if c != nil {
+				cb(c)
+			}
+		})
 	} else {
-		if uint64(-amount) > pb.value {
-			pb.value = 0
-		} else {
-			pb.value -= uint64(-amount)
-		}
-	}
-	pb.meta = meta
-	f.ndb.setPB(id, pb)
-	if c != nil {
-		c.balanceTracker.setBalance(pb.value, negBalance)
-		if !c.priority && pb.value > 0 {
-			// The capacity should be adjusted based on the requirement,
-			// but we have no idea about the new capacity, need a second
-			// call to update it.
-			c.priority = true
-			f.priorityConnected += c.capacity
-			c.balanceTracker.addCallback(balanceCallbackZero, 0, func() { f.balanceExhausted(id) })
-		}
-		// if balance is set to zero then reverting to non-priority status
-		// is handled by the balanceExhausted callback
-		c.balanceMetaInfo = meta
-	}
-	return oldBalance, pb.value, nil
-}
-
-// posBalance represents a recently accessed positive balance entry
-type posBalance struct {
-	value uint64
-	meta  string
-}
-
-// EncodeRLP implements rlp.Encoder
-func (e *posBalance) EncodeRLP(w io.Writer) error {
-	return rlp.Encode(w, []interface{}{e.value, e.meta})
-}
-
-// DecodeRLP implements rlp.Decoder
-func (e *posBalance) DecodeRLP(s *rlp.Stream) error {
-	var entry struct {
-		Value uint64
-		Meta  string
-	}
-	if err := s.Decode(&entry); err != nil {
-		return err
-	}
-	e.value = entry.Value
-	e.meta = entry.Meta
-	return nil
-}
-
-// negBalance represents a negative balance entry of a disconnected client
-type negBalance struct{ logValue int64 }
-
-// EncodeRLP implements rlp.Encoder
-func (e *negBalance) EncodeRLP(w io.Writer) error {
-	return rlp.Encode(w, []interface{}{uint64(e.logValue)})
-}
-
-// DecodeRLP implements rlp.Decoder
-func (e *negBalance) DecodeRLP(s *rlp.Stream) error {
-	var entry struct {
-		LogValue uint64
-	}
-	if err := s.Decode(&entry); err != nil {
-		return err
-	}
-	e.logValue = int64(entry.LogValue)
-	return nil
-}
-
-const (
-	// nodeDBVersion is the version identifier of the node data in db
-	//
-	// Changelog:
-	// * Replace `lastTotal` with `meta` in positive balance: version 0=>1
-	nodeDBVersion = 1
-
-	// dbCleanupCycle is the cycle of db for useless data cleanup
-	dbCleanupCycle = time.Hour
-)
-
-var (
-	positiveBalancePrefix    = []byte("pb:")             // dbVersion(uint16 big endian) + positiveBalancePrefix + id -> balance
-	negativeBalancePrefix    = []byte("nb:")             // dbVersion(uint16 big endian) + negativeBalancePrefix + ip -> balance
-	cumulativeRunningTimeKey = []byte("cumulativeTime:") // dbVersion(uint16 big endian) + cumulativeRunningTimeKey -> cumulativeTime
-)
-
-type nodeDB struct {
-	db              ethdb.Database
-	pcache          *lru.Cache
-	ncache          *lru.Cache
-	auxbuf          []byte                                // 37-byte auxiliary buffer for key encoding
-	verbuf          [2]byte                               // 2-byte auxiliary buffer for db version
-	nbEvictCallBack func(mclock.AbsTime, negBalance) bool // Callback to determine whether the negative balance can be evicted.
-	clock           mclock.Clock
-	closeCh         chan struct{}
-	cleanupHook     func() // Test hook used for testing
-}
-
-func newNodeDB(db ethdb.Database, clock mclock.Clock) *nodeDB {
-	pcache, _ := lru.New(posBalanceCacheLimit)
-	ncache, _ := lru.New(negBalanceCacheLimit)
-	ndb := &nodeDB{
-		db:      db,
-		pcache:  pcache,
-		ncache:  ncache,
-		auxbuf:  make([]byte, 37),
-		clock:   clock,
-		closeCh: make(chan struct{}),
-	}
-	binary.BigEndian.PutUint16(ndb.verbuf[:], uint16(nodeDBVersion))
-	go ndb.expirer()
-	return ndb
-}
-
-func (db *nodeDB) close() {
-	close(db.closeCh)
-}
-
-func (db *nodeDB) getPrefix(neg bool) []byte {
-	prefix := positiveBalancePrefix
-	if neg {
-		prefix = negativeBalancePrefix
-	}
-	return append(db.verbuf[:], prefix...)
-}
-
-func (db *nodeDB) key(id []byte, neg bool) []byte {
-	prefix := positiveBalancePrefix
-	if neg {
-		prefix = negativeBalancePrefix
-	}
-	if len(prefix)+len(db.verbuf)+len(id) > len(db.auxbuf) {
-		db.auxbuf = append(db.auxbuf, make([]byte, len(prefix)+len(db.verbuf)+len(id)-len(db.auxbuf))...)
-	}
-	copy(db.auxbuf[:len(db.verbuf)], db.verbuf[:])
-	copy(db.auxbuf[len(db.verbuf):len(db.verbuf)+len(prefix)], prefix)
-	copy(db.auxbuf[len(prefix)+len(db.verbuf):len(prefix)+len(db.verbuf)+len(id)], id)
-	return db.auxbuf[:len(prefix)+len(db.verbuf)+len(id)]
-}
-
-func (db *nodeDB) getCumulativeTime() int64 {
-	blob, err := db.db.Get(append(cumulativeRunningTimeKey, db.verbuf[:]...))
-	if err != nil || len(blob) == 0 {
-		return 0
-	}
-	return int64(binary.BigEndian.Uint64(blob))
-}
-
-func (db *nodeDB) setCumulativeTime(v int64) {
-	binary.BigEndian.PutUint64(db.auxbuf[:8], uint64(v))
-	db.db.Put(append(cumulativeRunningTimeKey, db.verbuf[:]...), db.auxbuf[:8])
-}
-
-func (db *nodeDB) getOrNewPB(id enode.ID) posBalance {
-	key := db.key(id.Bytes(), false)
-	item, exist := db.pcache.Get(string(key))
-	if exist {
-		return item.(posBalance)
-	}
-	var balance posBalance
-	if enc, err := db.db.Get(key); err == nil {
-		if err := rlp.DecodeBytes(enc, &balance); err != nil {
-			log.Error("Failed to decode positive balance", "err", err)
-		}
-	}
-	db.pcache.Add(string(key), balance)
-	return balance
-}
-
-func (db *nodeDB) setPB(id enode.ID, b posBalance) {
-	if b.value == 0 && len(b.meta) == 0 {
-		db.delPB(id)
-		return
-	}
-	key := db.key(id.Bytes(), false)
-	enc, err := rlp.EncodeToBytes(&(b))
-	if err != nil {
-		log.Error("Failed to encode positive balance", "err", err)
-		return
-	}
-	db.db.Put(key, enc)
-	db.pcache.Add(string(key), b)
-}
-
-func (db *nodeDB) delPB(id enode.ID) {
-	key := db.key(id.Bytes(), false)
-	db.db.Delete(key)
-	db.pcache.Remove(string(key))
-}
-
-// getPosBalanceIDs returns a lexicographically ordered list of IDs of accounts
-// with a positive balance
-func (db *nodeDB) getPosBalanceIDs(start, stop enode.ID, maxCount int) (result []enode.ID) {
-	if maxCount <= 0 {
-		return
-	}
-	prefix := db.getPrefix(false)
-	it := db.db.NewIterator(prefix, start.Bytes())
-	defer it.Release()
-	for i := len(stop[:]) - 1; i >= 0; i-- {
-		stop[i]--
-		if stop[i] != 255 {
-			break
-		}
-	}
-	stopKey := db.key(stop.Bytes(), false)
-	keyLen := len(stopKey)
-
-	for it.Next() {
-		var id enode.ID
-		if len(it.Key()) != keyLen || bytes.Compare(it.Key(), stopKey) == 1 {
-			return
-		}
-		copy(id[:], it.Key()[keyLen-len(id):])
-		result = append(result, id)
-		if len(result) == maxCount {
-			return
-		}
-	}
-	return
-}
-
-func (db *nodeDB) getOrNewNB(id string) negBalance {
-	key := db.key([]byte(id), true)
-	item, exist := db.ncache.Get(string(key))
-	if exist {
-		return item.(negBalance)
-	}
-	var balance negBalance
-	if enc, err := db.db.Get(key); err == nil {
-		if err := rlp.DecodeBytes(enc, &balance); err != nil {
-			log.Error("Failed to decode negative balance", "err", err)
-		}
-	}
-	db.ncache.Add(string(key), balance)
-	return balance
-}
-
-func (db *nodeDB) setNB(id string, b negBalance) {
-	key := db.key([]byte(id), true)
-	enc, err := rlp.EncodeToBytes(&(b))
-	if err != nil {
-		log.Error("Failed to encode negative balance", "err", err)
-		return
-	}
-	db.db.Put(key, enc)
-	db.ncache.Add(string(key), b)
-}
-
-func (db *nodeDB) delNB(id string) {
-	key := db.key([]byte(id), true)
-	db.db.Delete(key)
-	db.ncache.Remove(string(key))
-}
-
-func (db *nodeDB) expirer() {
-	for {
-		select {
-		case <-db.clock.After(dbCleanupCycle):
-			db.expireNodes()
-		case <-db.closeCh:
-			return
-		}
-	}
-}
-
-// expireNodes iterates the whole node db and checks whether the negative balance
-// entry can deleted.
-//
-// The rationale behind this is: server doesn't need to keep the negative balance
-// records if they are low enough.
-func (db *nodeDB) expireNodes() {
-	var (
-		visited int
-		deleted int
-		start   = time.Now()
-		prefix  = db.getPrefix(true)
-	)
-	iter := db.db.NewIterator(prefix, nil)
-	for iter.Next() {
-		visited += 1
-		var balance negBalance
-		if err := rlp.DecodeBytes(iter.Value(), &balance); err != nil {
-			log.Error("Failed to decode negative balance", "err", err)
-			continue
-		}
-		if db.nbEvictCallBack != nil && db.nbEvictCallBack(db.clock.Now(), balance) {
-			deleted += 1
-			db.db.Delete(iter.Key())
+		for _, id := range ids {
+			node := f.ns.GetNode(id)
+			if node == nil {
+				node = enode.SignNull(&enr.Record{}, id)
+			}
+			c, _ := f.ns.GetField(node, clientField).(*clientInfo)
+			if c != nil {
+				cb(c)
+			} else {
+				c = &clientInfo{node: node}
+				f.ns.SetField(node, clientField, c)
+				f.ns.SetField(node, connAddressField, "")
+				if c.balance, _ = f.ns.GetField(node, f.BalanceField).(*lps.NodeBalance); c.balance != nil {
+					cb(c)
+				} else {
+					log.Error("BalanceField is missing")
+				}
+				f.ns.SetField(node, connAddressField, nil)
+				f.ns.SetField(node, clientField, nil)
+			}
 		}
 	}
-	// Invoke testing hook if it's not nil.
-	if db.cleanupHook != nil {
-		db.cleanupHook()
-	}
-	log.Debug("Expire nodes", "visited", visited, "deleted", deleted, "elapsed", common.PrettyDuration(time.Since(start)))
 }
diff --git a/les/clientpool_test.go b/les/clientpool_test.go
index f8c4ef0c0c33fc4d4d322208b0cc7a86601881fa..cfd1486b437e281e35aafaab916cee0c8e5f07c1 100644
--- a/les/clientpool_test.go
+++ b/les/clientpool_test.go
@@ -17,17 +17,17 @@
 package les
 
 import (
-	"bytes"
 	"fmt"
-	"math"
 	"math/rand"
-	"reflect"
 	"testing"
 	"time"
 
 	"github.com/ethereum/go-ethereum/common/mclock"
 	"github.com/ethereum/go-ethereum/core/rawdb"
+	lps "github.com/ethereum/go-ethereum/les/lespay/server"
 	"github.com/ethereum/go-ethereum/p2p/enode"
+	"github.com/ethereum/go-ethereum/p2p/enr"
+	"github.com/ethereum/go-ethereum/p2p/nodestate"
 )
 
 func TestClientPoolL10C100Free(t *testing.T) {
@@ -56,29 +56,68 @@ func TestClientPoolL100C300P20(t *testing.T) {
 
 const testClientPoolTicks = 100000
 
-type poolTestPeer int
+type poolTestPeer struct {
+	node            *enode.Node
+	index           int
+	disconnCh       chan int
+	cap             uint64
+	inactiveAllowed bool
+}
 
-func (i poolTestPeer) ID() enode.ID {
-	return enode.ID{byte(i % 256), byte(i >> 8)}
+func newPoolTestPeer(i int, disconnCh chan int) *poolTestPeer {
+	return &poolTestPeer{
+		index:     i,
+		disconnCh: disconnCh,
+		node:      enode.SignNull(&enr.Record{}, enode.ID{byte(i % 256), byte(i >> 8)}),
+	}
 }
 
-func (i poolTestPeer) freeClientId() string {
-	return fmt.Sprintf("addr #%d", i)
+func (i *poolTestPeer) Node() *enode.Node {
+	return i.node
 }
 
-func (i poolTestPeer) updateCapacity(uint64) {}
+func (i *poolTestPeer) freeClientId() string {
+	return fmt.Sprintf("addr #%d", i.index)
+}
+
+func (i *poolTestPeer) updateCapacity(cap uint64) {
+	i.cap = cap
+}
 
-type poolTestPeerWithCap struct {
-	poolTestPeer
+func (i *poolTestPeer) freeze() {}
 
-	cap uint64
+func (i *poolTestPeer) allowInactive() bool {
+	return i.inactiveAllowed
 }
 
-func (i *poolTestPeerWithCap) updateCapacity(cap uint64) { i.cap = cap }
+func getBalance(pool *clientPool, p *poolTestPeer) (pos, neg uint64) {
+	temp := pool.ns.GetField(p.node, clientField) == nil
+	if temp {
+		pool.ns.SetField(p.node, connAddressField, p.freeClientId())
+	}
+	n, _ := pool.ns.GetField(p.node, pool.BalanceField).(*lps.NodeBalance)
+	pos, neg = n.GetBalance()
+	if temp {
+		pool.ns.SetField(p.node, connAddressField, nil)
+	}
+	return
+}
 
-func (i poolTestPeer) freezeClient() {}
+func addBalance(pool *clientPool, id enode.ID, amount int64) {
+	pool.forClients([]enode.ID{id}, func(c *clientInfo) {
+		c.balance.AddBalance(amount)
+	})
+}
 
-func testClientPool(t *testing.T, connLimit, clientCount, paidCount int, randomDisconnect bool) {
+func checkDiff(a, b uint64) bool {
+	maxDiff := (a + b) / 2000
+	if maxDiff < 1 {
+		maxDiff = 1
+	}
+	return a > b+maxDiff || b > a+maxDiff
+}
+
+func testClientPool(t *testing.T, activeLimit, clientCount, paidCount int, randomDisconnect bool) {
 	rand.Seed(time.Now().UnixNano())
 	var (
 		clock     mclock.Simulated
@@ -89,15 +128,15 @@ func testClientPool(t *testing.T, connLimit, clientCount, paidCount int, randomD
 		disconnFn = func(id enode.ID) {
 			disconnCh <- int(id[0]) + int(id[1])<<8
 		}
-		pool = newClientPool(db, 1, &clock, disconnFn)
+		pool = newClientPool(db, 1, 0, &clock, disconnFn)
 	)
-	pool.setConnectedBias(0)
-	pool.setLimits(connLimit, uint64(connLimit))
-	pool.setDefaultFactors(priceFactors{1, 0, 1}, priceFactors{1, 0, 1})
+
+	pool.setLimits(activeLimit, uint64(activeLimit))
+	pool.setDefaultFactors(lps.PriceFactors{TimeFactor: 1, CapacityFactor: 0, RequestFactor: 1}, lps.PriceFactors{TimeFactor: 1, CapacityFactor: 0, RequestFactor: 1})
 
 	// pool should accept new peers up to its connected limit
-	for i := 0; i < connLimit; i++ {
-		if pool.connect(poolTestPeer(i), 0) {
+	for i := 0; i < activeLimit; i++ {
+		if cap, _ := pool.connect(newPoolTestPeer(i, disconnCh)); cap != 0 {
 			connected[i] = true
 		} else {
 			t.Fatalf("Test peer #%d rejected", i)
@@ -111,28 +150,30 @@ func testClientPool(t *testing.T, connLimit, clientCount, paidCount int, randomD
 			// give a positive balance to some of the peers
 			amount := testClientPoolTicks / 2 * int64(time.Second) // enough for half of the simulation period
 			for i := 0; i < paidCount; i++ {
-				pool.addBalance(poolTestPeer(i).ID(), amount, "")
+				addBalance(pool, newPoolTestPeer(i, disconnCh).node.ID(), amount)
 			}
 		}
 
 		i := rand.Intn(clientCount)
 		if connected[i] {
 			if randomDisconnect {
-				pool.disconnect(poolTestPeer(i))
+				pool.disconnect(newPoolTestPeer(i, disconnCh))
 				connected[i] = false
 				connTicks[i] += tickCounter
 			}
 		} else {
-			if pool.connect(poolTestPeer(i), 0) {
+			if cap, _ := pool.connect(newPoolTestPeer(i, disconnCh)); cap != 0 {
 				connected[i] = true
 				connTicks[i] -= tickCounter
+			} else {
+				pool.disconnect(newPoolTestPeer(i, disconnCh))
 			}
 		}
 	pollDisconnects:
 		for {
 			select {
 			case i := <-disconnCh:
-				pool.disconnect(poolTestPeer(i))
+				pool.disconnect(newPoolTestPeer(i, disconnCh))
 				if connected[i] {
 					connTicks[i] += tickCounter
 					connected[i] = false
@@ -143,10 +184,10 @@ func testClientPool(t *testing.T, connLimit, clientCount, paidCount int, randomD
 		}
 	}
 
-	expTicks := testClientPoolTicks/2*connLimit/clientCount + testClientPoolTicks/2*(connLimit-paidCount)/(clientCount-paidCount)
+	expTicks := testClientPoolTicks/2*activeLimit/clientCount + testClientPoolTicks/2*(activeLimit-paidCount)/(clientCount-paidCount)
 	expMin := expTicks - expTicks/5
 	expMax := expTicks + expTicks/5
-	paidTicks := testClientPoolTicks/2*connLimit/clientCount + testClientPoolTicks/2
+	paidTicks := testClientPoolTicks/2*activeLimit/clientCount + testClientPoolTicks/2
 	paidMin := paidTicks - paidTicks/5
 	paidMax := paidTicks + paidTicks/5
 
@@ -167,22 +208,39 @@ func testClientPool(t *testing.T, connLimit, clientCount, paidCount int, randomD
 	pool.stop()
 }
 
+func testPriorityConnect(t *testing.T, pool *clientPool, p *poolTestPeer, cap uint64, expSuccess bool) {
+	if cap, _ := pool.connect(p); cap == 0 {
+		if expSuccess {
+			t.Fatalf("Failed to connect paid client")
+		} else {
+			return
+		}
+	}
+	if _, err := pool.setCapacity(p.node, "", cap, defaultConnectedBias, true); err != nil {
+		if expSuccess {
+			t.Fatalf("Failed to raise capacity of paid client")
+		} else {
+			return
+		}
+	}
+	if !expSuccess {
+		t.Fatalf("Should reject high capacity paid client")
+	}
+}
+
 func TestConnectPaidClient(t *testing.T) {
 	var (
 		clock mclock.Simulated
 		db    = rawdb.NewMemoryDatabase()
 	)
-	pool := newClientPool(db, 1, &clock, nil)
+	pool := newClientPool(db, 1, defaultConnectedBias, &clock, func(id enode.ID) {})
 	defer pool.stop()
 	pool.setLimits(10, uint64(10))
-	pool.setDefaultFactors(priceFactors{1, 0, 1}, priceFactors{1, 0, 1})
+	pool.setDefaultFactors(lps.PriceFactors{TimeFactor: 1, CapacityFactor: 0, RequestFactor: 1}, lps.PriceFactors{TimeFactor: 1, CapacityFactor: 0, RequestFactor: 1})
 
 	// Add balance for an external client and mark it as paid client
-	pool.addBalance(poolTestPeer(0).ID(), 1000, "")
-
-	if !pool.connect(poolTestPeer(0), 10) {
-		t.Fatalf("Failed to connect paid client")
-	}
+	addBalance(pool, newPoolTestPeer(0, nil).node.ID(), int64(time.Minute))
+	testPriorityConnect(t, pool, newPoolTestPeer(0, nil), 10, true)
 }
 
 func TestConnectPaidClientToSmallPool(t *testing.T) {
@@ -190,18 +248,16 @@ func TestConnectPaidClientToSmallPool(t *testing.T) {
 		clock mclock.Simulated
 		db    = rawdb.NewMemoryDatabase()
 	)
-	pool := newClientPool(db, 1, &clock, nil)
+	pool := newClientPool(db, 1, defaultConnectedBias, &clock, func(id enode.ID) {})
 	defer pool.stop()
 	pool.setLimits(10, uint64(10)) // Total capacity limit is 10
-	pool.setDefaultFactors(priceFactors{1, 0, 1}, priceFactors{1, 0, 1})
+	pool.setDefaultFactors(lps.PriceFactors{TimeFactor: 1, CapacityFactor: 0, RequestFactor: 1}, lps.PriceFactors{TimeFactor: 1, CapacityFactor: 0, RequestFactor: 1})
 
 	// Add balance for an external client and mark it as paid client
-	pool.addBalance(poolTestPeer(0).ID(), 1000, "")
+	addBalance(pool, newPoolTestPeer(0, nil).node.ID(), int64(time.Minute))
 
 	// Connect a fat paid client to pool, should reject it.
-	if pool.connect(poolTestPeer(0), 100) {
-		t.Fatalf("Connected fat paid client, should reject it")
-	}
+	testPriorityConnect(t, pool, newPoolTestPeer(0, nil), 100, false)
 }
 
 func TestConnectPaidClientToFullPool(t *testing.T) {
@@ -210,23 +266,23 @@ func TestConnectPaidClientToFullPool(t *testing.T) {
 		db    = rawdb.NewMemoryDatabase()
 	)
 	removeFn := func(enode.ID) {} // Noop
-	pool := newClientPool(db, 1, &clock, removeFn)
+	pool := newClientPool(db, 1, defaultConnectedBias, &clock, removeFn)
 	defer pool.stop()
 	pool.setLimits(10, uint64(10)) // Total capacity limit is 10
-	pool.setDefaultFactors(priceFactors{1, 0, 1}, priceFactors{1, 0, 1})
+	pool.setDefaultFactors(lps.PriceFactors{TimeFactor: 1, CapacityFactor: 0, RequestFactor: 1}, lps.PriceFactors{TimeFactor: 1, CapacityFactor: 0, RequestFactor: 1})
 
 	for i := 0; i < 10; i++ {
-		pool.addBalance(poolTestPeer(i).ID(), 1000000000, "")
-		pool.connect(poolTestPeer(i), 1)
+		addBalance(pool, newPoolTestPeer(i, nil).node.ID(), int64(time.Second*20))
+		pool.connect(newPoolTestPeer(i, nil))
 	}
-	pool.addBalance(poolTestPeer(11).ID(), 1000, "") // Add low balance to new paid client
-	if pool.connect(poolTestPeer(11), 1) {
+	addBalance(pool, newPoolTestPeer(11, nil).node.ID(), int64(time.Second*2)) // Add low balance to new paid client
+	if cap, _ := pool.connect(newPoolTestPeer(11, nil)); cap != 0 {
 		t.Fatalf("Low balance paid client should be rejected")
 	}
 	clock.Run(time.Second)
-	pool.addBalance(poolTestPeer(12).ID(), 1000000000*60*3, "") // Add high balance to new paid client
-	if !pool.connect(poolTestPeer(12), 1) {
-		t.Fatalf("High balance paid client should be accpected")
+	addBalance(pool, newPoolTestPeer(12, nil).node.ID(), int64(time.Minute*5)) // Add high balance to new paid client
+	if cap, _ := pool.connect(newPoolTestPeer(12, nil)); cap == 0 {
+		t.Fatalf("High balance paid client should be accepted")
 	}
 }
 
@@ -234,23 +290,25 @@ func TestPaidClientKickedOut(t *testing.T) {
 	var (
 		clock    mclock.Simulated
 		db       = rawdb.NewMemoryDatabase()
-		kickedCh = make(chan int, 1)
+		kickedCh = make(chan int, 100)
 	)
-	removeFn := func(id enode.ID) { kickedCh <- int(id[0]) }
-	pool := newClientPool(db, 1, &clock, removeFn)
+	removeFn := func(id enode.ID) {
+		kickedCh <- int(id[0])
+	}
+	pool := newClientPool(db, 1, defaultConnectedBias, &clock, removeFn)
+	pool.bt.SetExpirationTCs(0, 0)
 	defer pool.stop()
 	pool.setLimits(10, uint64(10)) // Total capacity limit is 10
-	pool.setDefaultFactors(priceFactors{1, 0, 1}, priceFactors{1, 0, 1})
+	pool.setDefaultFactors(lps.PriceFactors{TimeFactor: 1, CapacityFactor: 0, RequestFactor: 1}, lps.PriceFactors{TimeFactor: 1, CapacityFactor: 0, RequestFactor: 1})
 
 	for i := 0; i < 10; i++ {
-		pool.addBalance(poolTestPeer(i).ID(), 1000000000, "") // 1 second allowance
-		pool.connect(poolTestPeer(i), 1)
+		addBalance(pool, newPoolTestPeer(i, kickedCh).node.ID(), 10000000000) // 10 second allowance
+		pool.connect(newPoolTestPeer(i, kickedCh))
 		clock.Run(time.Millisecond)
 	}
-	clock.Run(time.Second)
-	clock.Run(defaultConnectedBias)
-	if !pool.connect(poolTestPeer(11), 0) {
-		t.Fatalf("Free client should be accectped")
+	clock.Run(defaultConnectedBias + time.Second*11)
+	if cap, _ := pool.connect(newPoolTestPeer(11, kickedCh)); cap == 0 {
+		t.Fatalf("Free client should be accepted")
 	}
 	select {
 	case id := <-kickedCh:
@@ -267,13 +325,14 @@ func TestConnectFreeClient(t *testing.T) {
 		clock mclock.Simulated
 		db    = rawdb.NewMemoryDatabase()
 	)
-	pool := newClientPool(db, 1, &clock, nil)
+	pool := newClientPool(db, 1, defaultConnectedBias, &clock, func(id enode.ID) {})
 	defer pool.stop()
 	pool.setLimits(10, uint64(10))
-	pool.setDefaultFactors(priceFactors{1, 0, 1}, priceFactors{1, 0, 1})
-	if !pool.connect(poolTestPeer(0), 10) {
+	pool.setDefaultFactors(lps.PriceFactors{TimeFactor: 1, CapacityFactor: 0, RequestFactor: 1}, lps.PriceFactors{TimeFactor: 1, CapacityFactor: 0, RequestFactor: 1})
+	if cap, _ := pool.connect(newPoolTestPeer(0, nil)); cap == 0 {
 		t.Fatalf("Failed to connect free client")
 	}
+	testPriorityConnect(t, pool, newPoolTestPeer(0, nil), 2, false)
 }
 
 func TestConnectFreeClientToFullPool(t *testing.T) {
@@ -282,24 +341,24 @@ func TestConnectFreeClientToFullPool(t *testing.T) {
 		db    = rawdb.NewMemoryDatabase()
 	)
 	removeFn := func(enode.ID) {} // Noop
-	pool := newClientPool(db, 1, &clock, removeFn)
+	pool := newClientPool(db, 1, defaultConnectedBias, &clock, removeFn)
 	defer pool.stop()
 	pool.setLimits(10, uint64(10)) // Total capacity limit is 10
-	pool.setDefaultFactors(priceFactors{1, 0, 1}, priceFactors{1, 0, 1})
+	pool.setDefaultFactors(lps.PriceFactors{TimeFactor: 1, CapacityFactor: 0, RequestFactor: 1}, lps.PriceFactors{TimeFactor: 1, CapacityFactor: 0, RequestFactor: 1})
 
 	for i := 0; i < 10; i++ {
-		pool.connect(poolTestPeer(i), 1)
+		pool.connect(newPoolTestPeer(i, nil))
 	}
-	if pool.connect(poolTestPeer(11), 1) {
+	if cap, _ := pool.connect(newPoolTestPeer(11, nil)); cap != 0 {
 		t.Fatalf("New free client should be rejected")
 	}
 	clock.Run(time.Minute)
-	if pool.connect(poolTestPeer(12), 1) {
+	if cap, _ := pool.connect(newPoolTestPeer(12, nil)); cap != 0 {
 		t.Fatalf("New free client should be rejected")
 	}
 	clock.Run(time.Millisecond)
 	clock.Run(4 * time.Minute)
-	if !pool.connect(poolTestPeer(13), 1) {
+	if cap, _ := pool.connect(newPoolTestPeer(13, nil)); cap == 0 {
 		t.Fatalf("Old client connects more than 5min should be kicked")
 	}
 }
@@ -308,24 +367,30 @@ func TestFreeClientKickedOut(t *testing.T) {
 	var (
 		clock  mclock.Simulated
 		db     = rawdb.NewMemoryDatabase()
-		kicked = make(chan int, 10)
+		kicked = make(chan int, 100)
 	)
 	removeFn := func(id enode.ID) { kicked <- int(id[0]) }
-	pool := newClientPool(db, 1, &clock, removeFn)
+	pool := newClientPool(db, 1, defaultConnectedBias, &clock, removeFn)
 	defer pool.stop()
 	pool.setLimits(10, uint64(10)) // Total capacity limit is 10
-	pool.setDefaultFactors(priceFactors{1, 0, 1}, priceFactors{1, 0, 1})
+	pool.setDefaultFactors(lps.PriceFactors{TimeFactor: 1, CapacityFactor: 0, RequestFactor: 1}, lps.PriceFactors{TimeFactor: 1, CapacityFactor: 0, RequestFactor: 1})
 
 	for i := 0; i < 10; i++ {
-		pool.connect(poolTestPeer(i), 1)
+		pool.connect(newPoolTestPeer(i, kicked))
 		clock.Run(time.Millisecond)
 	}
-	if pool.connect(poolTestPeer(10), 1) {
+	if cap, _ := pool.connect(newPoolTestPeer(10, kicked)); cap != 0 {
 		t.Fatalf("New free client should be rejected")
 	}
+	select {
+	case <-kicked:
+	case <-time.NewTimer(time.Second).C:
+		t.Fatalf("timeout")
+	}
+	pool.disconnect(newPoolTestPeer(10, kicked))
 	clock.Run(5 * time.Minute)
 	for i := 0; i < 10; i++ {
-		pool.connect(poolTestPeer(i+10), 1)
+		pool.connect(newPoolTestPeer(i+10, kicked))
 	}
 	for i := 0; i < 10; i++ {
 		select {
@@ -346,19 +411,19 @@ func TestPositiveBalanceCalculation(t *testing.T) {
 		kicked = make(chan int, 10)
 	)
 	removeFn := func(id enode.ID) { kicked <- int(id[0]) } // Noop
-	pool := newClientPool(db, 1, &clock, removeFn)
+	pool := newClientPool(db, 1, defaultConnectedBias, &clock, removeFn)
 	defer pool.stop()
 	pool.setLimits(10, uint64(10)) // Total capacity limit is 10
-	pool.setDefaultFactors(priceFactors{1, 0, 1}, priceFactors{1, 0, 1})
+	pool.setDefaultFactors(lps.PriceFactors{TimeFactor: 1, CapacityFactor: 0, RequestFactor: 1}, lps.PriceFactors{TimeFactor: 1, CapacityFactor: 0, RequestFactor: 1})
 
-	pool.addBalance(poolTestPeer(0).ID(), int64(time.Minute*3), "")
-	pool.connect(poolTestPeer(0), 10)
+	addBalance(pool, newPoolTestPeer(0, kicked).node.ID(), int64(time.Minute*3))
+	testPriorityConnect(t, pool, newPoolTestPeer(0, kicked), 10, true)
 	clock.Run(time.Minute)
 
-	pool.disconnect(poolTestPeer(0))
-	pb := pool.ndb.getOrNewPB(poolTestPeer(0).ID())
-	if pb.value != uint64(time.Minute*2) {
-		t.Fatalf("Positive balance mismatch, want %v, got %v", uint64(time.Minute*2), pb.value)
+	pool.disconnect(newPoolTestPeer(0, kicked))
+	pb, _ := getBalance(pool, newPoolTestPeer(0, kicked))
+	if checkDiff(pb, uint64(time.Minute*2)) {
+		t.Fatalf("Positive balance mismatch, want %v, got %v", uint64(time.Minute*2), pb)
 	}
 }
 
@@ -369,18 +434,16 @@ func TestDowngradePriorityClient(t *testing.T) {
 		kicked = make(chan int, 10)
 	)
 	removeFn := func(id enode.ID) { kicked <- int(id[0]) } // Noop
-	pool := newClientPool(db, 1, &clock, removeFn)
+	pool := newClientPool(db, 1, defaultConnectedBias, &clock, removeFn)
 	defer pool.stop()
 	pool.setLimits(10, uint64(10)) // Total capacity limit is 10
-	pool.setDefaultFactors(priceFactors{1, 0, 1}, priceFactors{1, 0, 1})
+	pool.setDefaultFactors(lps.PriceFactors{TimeFactor: 1, CapacityFactor: 0, RequestFactor: 1}, lps.PriceFactors{TimeFactor: 1, CapacityFactor: 0, RequestFactor: 1})
 
-	p := &poolTestPeerWithCap{
-		poolTestPeer: poolTestPeer(0),
-	}
-	pool.addBalance(p.ID(), int64(time.Minute), "")
-	pool.connect(p, 10)
+	p := newPoolTestPeer(0, kicked)
+	addBalance(pool, p.node.ID(), int64(time.Minute))
+	testPriorityConnect(t, pool, p, 10, true)
 	if p.cap != 10 {
-		t.Fatalf("The capcacity of priority peer hasn't been updated, got: %d", p.cap)
+		t.Fatalf("The capacity of priority peer hasn't been updated, got: %d", p.cap)
 	}
 
 	clock.Run(time.Minute)             // All positive balance should be used up.
@@ -388,156 +451,131 @@ func TestDowngradePriorityClient(t *testing.T) {
 	if p.cap != 1 {
 		t.Fatalf("The capcacity of peer should be downgraded, got: %d", p.cap)
 	}
-	pb := pool.ndb.getOrNewPB(poolTestPeer(0).ID())
-	if pb.value != 0 {
-		t.Fatalf("Positive balance mismatch, want %v, got %v", 0, pb.value)
+	pb, _ := getBalance(pool, newPoolTestPeer(0, kicked))
+	if pb != 0 {
+		t.Fatalf("Positive balance mismatch, want %v, got %v", 0, pb)
 	}
 
-	pool.addBalance(poolTestPeer(0).ID(), int64(time.Minute), "")
-	pb = pool.ndb.getOrNewPB(poolTestPeer(0).ID())
-	if pb.value != uint64(time.Minute) {
-		t.Fatalf("Positive balance mismatch, want %v, got %v", uint64(time.Minute), pb.value)
+	addBalance(pool, newPoolTestPeer(0, kicked).node.ID(), int64(time.Minute))
+	pb, _ = getBalance(pool, newPoolTestPeer(0, kicked))
+	if checkDiff(pb, uint64(time.Minute)) {
+		t.Fatalf("Positive balance mismatch, want %v, got %v", uint64(time.Minute), pb)
 	}
 }
 
 func TestNegativeBalanceCalculation(t *testing.T) {
 	var (
-		clock  mclock.Simulated
-		db     = rawdb.NewMemoryDatabase()
-		kicked = make(chan int, 10)
+		clock mclock.Simulated
+		db    = rawdb.NewMemoryDatabase()
 	)
-	removeFn := func(id enode.ID) { kicked <- int(id[0]) } // Noop
-	pool := newClientPool(db, 1, &clock, removeFn)
+	pool := newClientPool(db, 1, defaultConnectedBias, &clock, func(id enode.ID) {})
 	defer pool.stop()
 	pool.setLimits(10, uint64(10)) // Total capacity limit is 10
-	pool.setDefaultFactors(priceFactors{1, 0, 1}, priceFactors{1, 0, 1})
+	pool.setDefaultFactors(lps.PriceFactors{TimeFactor: 1e-3, CapacityFactor: 0, RequestFactor: 1}, lps.PriceFactors{TimeFactor: 1e-3, CapacityFactor: 0, RequestFactor: 1})
 
 	for i := 0; i < 10; i++ {
-		pool.connect(poolTestPeer(i), 1)
+		pool.connect(newPoolTestPeer(i, nil))
 	}
 	clock.Run(time.Second)
 
 	for i := 0; i < 10; i++ {
-		pool.disconnect(poolTestPeer(i))
-		nb := pool.ndb.getOrNewNB(poolTestPeer(i).freeClientId())
-		if nb.logValue != 0 {
+		pool.disconnect(newPoolTestPeer(i, nil))
+		_, nb := getBalance(pool, newPoolTestPeer(i, nil))
+		if nb != 0 {
 			t.Fatalf("Short connection shouldn't be recorded")
 		}
 	}
-
 	for i := 0; i < 10; i++ {
-		pool.connect(poolTestPeer(i), 1)
+		pool.connect(newPoolTestPeer(i, nil))
 	}
 	clock.Run(time.Minute)
 	for i := 0; i < 10; i++ {
-		pool.disconnect(poolTestPeer(i))
-		nb := pool.ndb.getOrNewNB(poolTestPeer(i).freeClientId())
-		nb.logValue -= pool.logOffset(clock.Now())
-		nb.logValue /= fixedPointMultiplier
-		if nb.logValue != int64(math.Log(float64(time.Minute/time.Second))) {
-			t.Fatalf("Negative balance mismatch, want %v, got %v", int64(math.Log(float64(time.Minute/time.Second))), nb.logValue)
+		pool.disconnect(newPoolTestPeer(i, nil))
+		_, nb := getBalance(pool, newPoolTestPeer(i, nil))
+		if checkDiff(nb, uint64(time.Minute)/1000) {
+			t.Fatalf("Negative balance mismatch, want %v, got %v", uint64(time.Minute)/1000, nb)
 		}
 	}
 }
 
-func TestNodeDB(t *testing.T) {
-	ndb := newNodeDB(rawdb.NewMemoryDatabase(), mclock.System{})
-	defer ndb.close()
-
-	if !bytes.Equal(ndb.verbuf[:], []byte{0x00, nodeDBVersion}) {
-		t.Fatalf("version buffer mismatch, want %v, got %v", []byte{0x00, nodeDBVersion}, ndb.verbuf)
-	}
-	var cases = []struct {
-		id       enode.ID
-		ip       string
-		balance  interface{}
-		positive bool
-	}{
-		{enode.ID{0x00, 0x01, 0x02}, "", posBalance{value: 100}, true},
-		{enode.ID{0x00, 0x01, 0x02}, "", posBalance{value: 200}, true},
-		{enode.ID{}, "127.0.0.1", negBalance{logValue: 10}, false},
-		{enode.ID{}, "127.0.0.1", negBalance{logValue: 20}, false},
-	}
-	for _, c := range cases {
-		if c.positive {
-			ndb.setPB(c.id, c.balance.(posBalance))
-			if pb := ndb.getOrNewPB(c.id); !reflect.DeepEqual(pb, c.balance.(posBalance)) {
-				t.Fatalf("Positive balance mismatch, want %v, got %v", c.balance.(posBalance), pb)
-			}
-		} else {
-			ndb.setNB(c.ip, c.balance.(negBalance))
-			if nb := ndb.getOrNewNB(c.ip); !reflect.DeepEqual(nb, c.balance.(negBalance)) {
-				t.Fatalf("Negative balance mismatch, want %v, got %v", c.balance.(negBalance), nb)
-			}
-		}
-	}
-	for _, c := range cases {
-		if c.positive {
-			ndb.delPB(c.id)
-			if pb := ndb.getOrNewPB(c.id); !reflect.DeepEqual(pb, posBalance{}) {
-				t.Fatalf("Positive balance mismatch, want %v, got %v", posBalance{}, pb)
-			}
-		} else {
-			ndb.delNB(c.ip)
-			if nb := ndb.getOrNewNB(c.ip); !reflect.DeepEqual(nb, negBalance{}) {
-				t.Fatalf("Negative balance mismatch, want %v, got %v", negBalance{}, nb)
-			}
-		}
-	}
-	ndb.setCumulativeTime(100)
-	if ndb.getCumulativeTime() != 100 {
-		t.Fatalf("Cumulative time mismatch, want %v, got %v", 100, ndb.getCumulativeTime())
-	}
-}
-
-func TestNodeDBExpiration(t *testing.T) {
+func TestInactiveClient(t *testing.T) {
 	var (
-		iterated int
-		done     = make(chan struct{}, 1)
+		clock mclock.Simulated
+		db    = rawdb.NewMemoryDatabase()
 	)
-	callback := func(now mclock.AbsTime, b negBalance) bool {
-		iterated += 1
-		return true
-	}
-	clock := &mclock.Simulated{}
-	ndb := newNodeDB(rawdb.NewMemoryDatabase(), clock)
-	defer ndb.close()
-	ndb.nbEvictCallBack = callback
-	ndb.cleanupHook = func() { done <- struct{}{} }
-
-	var cases = []struct {
-		ip      string
-		balance negBalance
-	}{
-		{"127.0.0.1", negBalance{logValue: 1}},
-		{"127.0.0.2", negBalance{logValue: 1}},
-		{"127.0.0.3", negBalance{logValue: 1}},
-		{"127.0.0.4", negBalance{logValue: 1}},
-	}
-	for _, c := range cases {
-		ndb.setNB(c.ip, c.balance)
-	}
-	clock.WaitForTimers(1)
-	clock.Run(time.Hour + time.Minute)
-	select {
-	case <-done:
-	case <-time.NewTimer(time.Second).C:
-		t.Fatalf("timeout")
-	}
-	if iterated != 4 {
-		t.Fatalf("Failed to evict useless negative balances, want %v, got %d", 4, iterated)
-	}
-	clock.WaitForTimers(1)
-	for _, c := range cases {
-		ndb.setNB(c.ip, c.balance)
-	}
-	clock.Run(time.Hour + time.Minute)
-	select {
-	case <-done:
-	case <-time.NewTimer(time.Second).C:
-		t.Fatalf("timeout")
-	}
-	if iterated != 8 {
-		t.Fatalf("Failed to evict useless negative balances, want %v, got %d", 4, iterated)
+	pool := newClientPool(db, 1, defaultConnectedBias, &clock, func(id enode.ID) {})
+	defer pool.stop()
+	pool.setLimits(2, uint64(2))
+
+	p1 := newPoolTestPeer(1, nil)
+	p1.inactiveAllowed = true
+	p2 := newPoolTestPeer(2, nil)
+	p2.inactiveAllowed = true
+	p3 := newPoolTestPeer(3, nil)
+	p3.inactiveAllowed = true
+	addBalance(pool, p1.node.ID(), 1000*int64(time.Second))
+	addBalance(pool, p3.node.ID(), 2000*int64(time.Second))
+	// p1: 1000  p2: 0  p3: 2000
+	p1.cap, _ = pool.connect(p1)
+	if p1.cap != 1 {
+		t.Fatalf("Failed to connect peer #1")
+	}
+	p2.cap, _ = pool.connect(p2)
+	if p2.cap != 1 {
+		t.Fatalf("Failed to connect peer #2")
+	}
+	p3.cap, _ = pool.connect(p3)
+	if p3.cap != 1 {
+		t.Fatalf("Failed to connect peer #3")
+	}
+	if p2.cap != 0 {
+		t.Fatalf("Failed to deactivate peer #2")
+	}
+	addBalance(pool, p2.node.ID(), 3000*int64(time.Second))
+	// p1: 1000  p2: 3000  p3: 2000
+	if p2.cap != 1 {
+		t.Fatalf("Failed to activate peer #2")
+	}
+	if p1.cap != 0 {
+		t.Fatalf("Failed to deactivate peer #1")
+	}
+	addBalance(pool, p2.node.ID(), -2500*int64(time.Second))
+	// p1: 1000  p2: 500  p3: 2000
+	if p1.cap != 1 {
+		t.Fatalf("Failed to activate peer #1")
+	}
+	if p2.cap != 0 {
+		t.Fatalf("Failed to deactivate peer #2")
+	}
+	pool.setDefaultFactors(lps.PriceFactors{TimeFactor: 1, CapacityFactor: 0, RequestFactor: 0}, lps.PriceFactors{TimeFactor: 1, CapacityFactor: 0, RequestFactor: 0})
+	p4 := newPoolTestPeer(4, nil)
+	addBalance(pool, p4.node.ID(), 1500*int64(time.Second))
+	// p1: 1000  p2: 500  p3: 2000  p4: 1500
+	p4.cap, _ = pool.connect(p4)
+	if p4.cap != 1 {
+		t.Fatalf("Failed to activate peer #4")
+	}
+	if p1.cap != 0 {
+		t.Fatalf("Failed to deactivate peer #1")
+	}
+	clock.Run(time.Second * 600)
+	// manually trigger a check to avoid a long real-time wait
+	pool.ns.SetState(p1.node, pool.UpdateFlag, nodestate.Flags{}, 0)
+	pool.ns.SetState(p1.node, nodestate.Flags{}, pool.UpdateFlag, 0)
+	// p1: 1000  p2: 500  p3: 2000  p4: 900
+	if p1.cap != 1 {
+		t.Fatalf("Failed to activate peer #1")
+	}
+	if p4.cap != 0 {
+		t.Fatalf("Failed to deactivate peer #4")
+	}
+	pool.disconnect(p2)
+	pool.disconnect(p4)
+	addBalance(pool, p1.node.ID(), -1000*int64(time.Second))
+	if p1.cap != 1 {
+		t.Fatalf("Should not deactivate peer #1")
+	}
+	if p2.cap != 0 {
+		t.Fatalf("Should not activate peer #2")
 	}
 }
diff --git a/les/lespay/server/balance.go b/les/lespay/server/balance.go
new file mode 100644
index 0000000000000000000000000000000000000000..f820a4ad050de62d308e158355595b47ff5dba1c
--- /dev/null
+++ b/les/lespay/server/balance.go
@@ -0,0 +1,609 @@
+// Copyright 2019 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
+
+package server
+
+import (
+	"errors"
+	"math"
+	"sync"
+	"time"
+
+	"github.com/ethereum/go-ethereum/common/mclock"
+	"github.com/ethereum/go-ethereum/les/utils"
+	"github.com/ethereum/go-ethereum/p2p/enode"
+	"github.com/ethereum/go-ethereum/p2p/nodestate"
+)
+
+var errBalanceOverflow = errors.New("balance overflow")
+
+const maxBalance = math.MaxInt64 // maximum allowed balance value
+
+const (
+	balanceCallbackUpdate = iota // called when priority drops below the last minimum estimate
+	balanceCallbackZero          // called when priority drops to zero (positive balance exhausted)
+	balanceCallbackCount         // total number of balance callbacks
+)
+
+// PriceFactors determine the pricing policy (may apply either to positive or
+// negative balances which may have different factors).
+// - TimeFactor is cost unit per nanosecond of connection time
+// - CapacityFactor is cost unit per nanosecond of connection time per 1000000 capacity
+// - RequestFactor is cost unit per request "realCost" unit
+type PriceFactors struct {
+	TimeFactor, CapacityFactor, RequestFactor float64
+}
+
+// timePrice returns the price of connection per nanosecond at the given capacity
+func (p PriceFactors) timePrice(cap uint64) float64 {
+	return p.TimeFactor + float64(cap)*p.CapacityFactor/1000000
+}
+
+// NodeBalance keeps track of the positive and negative balances of a connected
+// client and calculates actual and projected future priority values.
+// Implements nodePriority interface.
+type NodeBalance struct {
+	bt                               *BalanceTracker
+	lock                             sync.RWMutex
+	node                             *enode.Node
+	connAddress                      string
+	active                           bool
+	priority                         bool
+	capacity                         uint64
+	balance                          balance
+	posFactor, negFactor             PriceFactors
+	sumReqCost                       uint64
+	lastUpdate, nextUpdate, initTime mclock.AbsTime
+	updateEvent                      mclock.Timer
+	// since only a limited and fixed number of callbacks are needed, they are
+	// stored in a fixed size array ordered by priority threshold.
+	callbacks [balanceCallbackCount]balanceCallback
+	// callbackIndex maps balanceCallback constants to callbacks array indexes (-1 if not active)
+	callbackIndex [balanceCallbackCount]int
+	callbackCount int // number of active callbacks
+}
+
+// balance represents a pair of positive and negative balances
+type balance struct {
+	pos, neg utils.ExpiredValue
+}
+
+// balanceCallback represents a single callback that is activated when client priority
+// reaches the given threshold
+type balanceCallback struct {
+	id        int
+	threshold int64
+	callback  func()
+}
+
+// GetBalance returns the current positive and negative balance.
+func (n *NodeBalance) GetBalance() (uint64, uint64) {
+	n.lock.Lock()
+	defer n.lock.Unlock()
+
+	now := n.bt.clock.Now()
+	n.updateBalance(now)
+	return n.balance.pos.Value(n.bt.posExp.LogOffset(now)), n.balance.neg.Value(n.bt.negExp.LogOffset(now))
+}
+
+// GetRawBalance returns the current positive and negative balance
+// but in the raw(expired value) format.
+func (n *NodeBalance) GetRawBalance() (utils.ExpiredValue, utils.ExpiredValue) {
+	n.lock.Lock()
+	defer n.lock.Unlock()
+
+	now := n.bt.clock.Now()
+	n.updateBalance(now)
+	return n.balance.pos, n.balance.neg
+}
+
+// AddBalance adds the given amount to the positive balance and returns the balance
+// before and after the operation. Exceeding maxBalance results in an error (balance is
+// unchanged) while adding a negative amount higher than the current balance results in
+// zero balance.
+func (n *NodeBalance) AddBalance(amount int64) (uint64, uint64, error) {
+	var (
+		err      error
+		old, new uint64
+	)
+	n.bt.ns.Operation(func() {
+		var (
+			callbacks   []func()
+			setPriority bool
+		)
+		n.bt.updateTotalBalance(n, func() bool {
+			now := n.bt.clock.Now()
+			n.updateBalance(now)
+
+			// Ensure the given amount is valid to apply.
+			offset := n.bt.posExp.LogOffset(now)
+			old = n.balance.pos.Value(offset)
+			if amount > 0 && (amount > maxBalance || old > maxBalance-uint64(amount)) {
+				err = errBalanceOverflow
+				return false
+			}
+
+			// Update the total positive balance counter.
+			n.balance.pos.Add(amount, offset)
+			callbacks = n.checkCallbacks(now)
+			setPriority = n.checkPriorityStatus()
+			new = n.balance.pos.Value(offset)
+			n.storeBalance(true, false)
+			return true
+		})
+		for _, cb := range callbacks {
+			cb()
+		}
+		if setPriority {
+			n.bt.ns.SetStateSub(n.node, n.bt.PriorityFlag, nodestate.Flags{}, 0)
+		}
+		n.signalPriorityUpdate()
+	})
+	if err != nil {
+		return old, old, err
+	}
+
+	return old, new, nil
+}
+
+// SetBalance sets the positive and negative balance to the given values
+func (n *NodeBalance) SetBalance(pos, neg uint64) error {
+	if pos > maxBalance || neg > maxBalance {
+		return errBalanceOverflow
+	}
+	n.bt.ns.Operation(func() {
+		var (
+			callbacks   []func()
+			setPriority bool
+		)
+		n.bt.updateTotalBalance(n, func() bool {
+			now := n.bt.clock.Now()
+			n.updateBalance(now)
+
+			var pb, nb utils.ExpiredValue
+			pb.Add(int64(pos), n.bt.posExp.LogOffset(now))
+			nb.Add(int64(neg), n.bt.negExp.LogOffset(now))
+			n.balance.pos = pb
+			n.balance.neg = nb
+			callbacks = n.checkCallbacks(now)
+			setPriority = n.checkPriorityStatus()
+			n.storeBalance(true, true)
+			return true
+		})
+		for _, cb := range callbacks {
+			cb()
+		}
+		if setPriority {
+			n.bt.ns.SetStateSub(n.node, n.bt.PriorityFlag, nodestate.Flags{}, 0)
+		}
+		n.signalPriorityUpdate()
+	})
+	return nil
+}
+
+// RequestServed should be called after serving a request for the given peer
+func (n *NodeBalance) RequestServed(cost uint64) uint64 {
+	n.lock.Lock()
+	var callbacks []func()
+	defer func() {
+		n.lock.Unlock()
+		if callbacks != nil {
+			n.bt.ns.Operation(func() {
+				for _, cb := range callbacks {
+					cb()
+				}
+			})
+		}
+	}()
+
+	now := n.bt.clock.Now()
+	n.updateBalance(now)
+	fcost := float64(cost)
+
+	posExp := n.bt.posExp.LogOffset(now)
+	var check bool
+	if !n.balance.pos.IsZero() {
+		if n.posFactor.RequestFactor != 0 {
+			c := -int64(fcost * n.posFactor.RequestFactor)
+			cc := n.balance.pos.Add(c, posExp)
+			if c == cc {
+				fcost = 0
+			} else {
+				fcost *= 1 - float64(cc)/float64(c)
+			}
+			check = true
+		} else {
+			fcost = 0
+		}
+	}
+	if fcost > 0 {
+		if n.negFactor.RequestFactor != 0 {
+			n.balance.neg.Add(int64(fcost*n.negFactor.RequestFactor), n.bt.negExp.LogOffset(now))
+			check = true
+		}
+	}
+	if check {
+		callbacks = n.checkCallbacks(now)
+	}
+	n.sumReqCost += cost
+	return n.balance.pos.Value(posExp)
+}
+
+// Priority returns the actual priority based on the current balance
+func (n *NodeBalance) Priority(now mclock.AbsTime, capacity uint64) int64 {
+	n.lock.Lock()
+	defer n.lock.Unlock()
+
+	n.updateBalance(now)
+	return n.balanceToPriority(n.balance, capacity)
+}
+
+// EstMinPriority gives a lower estimate for the priority at a given time in the future.
+// An average request cost per time is assumed that is twice the average cost per time
+// in the current session.
+// If update is true then a priority callback is added that turns UpdateFlag on and off
+// in case the priority goes below the estimated minimum.
+func (n *NodeBalance) EstMinPriority(at mclock.AbsTime, capacity uint64, update bool) int64 {
+	n.lock.Lock()
+	defer n.lock.Unlock()
+
+	var avgReqCost float64
+	dt := time.Duration(n.lastUpdate - n.initTime)
+	if dt > time.Second {
+		avgReqCost = float64(n.sumReqCost) * 2 / float64(dt)
+	}
+	pri := n.balanceToPriority(n.reducedBalance(at, capacity, avgReqCost), capacity)
+	if update {
+		n.addCallback(balanceCallbackUpdate, pri, n.signalPriorityUpdate)
+	}
+	return pri
+}
+
+// PosBalanceMissing calculates the missing amount of positive balance in order to
+// connect at targetCapacity, stay connected for the given amount of time and then
+// still have a priority of targetPriority
+func (n *NodeBalance) PosBalanceMissing(targetPriority int64, targetCapacity uint64, after time.Duration) uint64 {
+	n.lock.Lock()
+	defer n.lock.Unlock()
+
+	now := n.bt.clock.Now()
+	if targetPriority < 0 {
+		timePrice := n.negFactor.timePrice(targetCapacity)
+		timeCost := uint64(float64(after) * timePrice)
+		negBalance := n.balance.neg.Value(n.bt.negExp.LogOffset(now))
+		if timeCost+negBalance < uint64(-targetPriority) {
+			return 0
+		}
+		if uint64(-targetPriority) > negBalance && timePrice > 1e-100 {
+			if negTime := time.Duration(float64(uint64(-targetPriority)-negBalance) / timePrice); negTime < after {
+				after -= negTime
+			} else {
+				after = 0
+			}
+		}
+		targetPriority = 0
+	}
+	timePrice := n.posFactor.timePrice(targetCapacity)
+	posRequired := uint64(float64(targetPriority)*float64(targetCapacity)+float64(after)*timePrice) + 1
+	if posRequired >= maxBalance {
+		return math.MaxUint64 // target not reachable
+	}
+	posBalance := n.balance.pos.Value(n.bt.posExp.LogOffset(now))
+	if posRequired > posBalance {
+		return posRequired - posBalance
+	}
+	return 0
+}
+
+// SetPriceFactors sets the price factors. TimeFactor is the price of a nanosecond of
+// connection while RequestFactor is the price of a request cost unit.
+func (n *NodeBalance) SetPriceFactors(posFactor, negFactor PriceFactors) {
+	n.lock.Lock()
+	now := n.bt.clock.Now()
+	n.updateBalance(now)
+	n.posFactor, n.negFactor = posFactor, negFactor
+	callbacks := n.checkCallbacks(now)
+	n.lock.Unlock()
+	if callbacks != nil {
+		n.bt.ns.Operation(func() {
+			for _, cb := range callbacks {
+				cb()
+			}
+		})
+	}
+}
+
+// GetPriceFactors returns the price factors
+func (n *NodeBalance) GetPriceFactors() (posFactor, negFactor PriceFactors) {
+	n.lock.Lock()
+	defer n.lock.Unlock()
+
+	return n.posFactor, n.negFactor
+}
+
+// activate starts time/capacity cost deduction.
+func (n *NodeBalance) activate() {
+	n.bt.updateTotalBalance(n, func() bool {
+		if n.active {
+			return false
+		}
+		n.active = true
+		n.lastUpdate = n.bt.clock.Now()
+		return true
+	})
+}
+
+// deactivate stops time/capacity cost deduction and saves the balances in the database
+func (n *NodeBalance) deactivate() {
+	n.bt.updateTotalBalance(n, func() bool {
+		if !n.active {
+			return false
+		}
+		n.updateBalance(n.bt.clock.Now())
+		if n.updateEvent != nil {
+			n.updateEvent.Stop()
+			n.updateEvent = nil
+		}
+		n.storeBalance(true, true)
+		n.active = false
+		return true
+	})
+}
+
+// updateBalance updates balance based on the time factor
+func (n *NodeBalance) updateBalance(now mclock.AbsTime) {
+	if n.active && now > n.lastUpdate {
+		n.balance = n.reducedBalance(now, n.capacity, 0)
+		n.lastUpdate = now
+	}
+}
+
+// storeBalance stores the positive and/or negative balance of the node in the database
+func (n *NodeBalance) storeBalance(pos, neg bool) {
+	if pos {
+		n.bt.storeBalance(n.node.ID().Bytes(), false, n.balance.pos)
+	}
+	if neg {
+		n.bt.storeBalance([]byte(n.connAddress), true, n.balance.neg)
+	}
+}
+
+// addCallback sets up a one-time callback to be called when priority reaches
+// the threshold. If it has already reached the threshold the callback is called
+// immediately.
+// Note: should be called while n.lock is held
+// Note 2: the callback function runs inside a NodeStateMachine operation
+func (n *NodeBalance) addCallback(id int, threshold int64, callback func()) {
+	n.removeCallback(id)
+	idx := 0
+	for idx < n.callbackCount && threshold > n.callbacks[idx].threshold {
+		idx++
+	}
+	for i := n.callbackCount - 1; i >= idx; i-- {
+		n.callbackIndex[n.callbacks[i].id]++
+		n.callbacks[i+1] = n.callbacks[i]
+	}
+	n.callbackCount++
+	n.callbackIndex[id] = idx
+	n.callbacks[idx] = balanceCallback{id, threshold, callback}
+	now := n.bt.clock.Now()
+	n.updateBalance(now)
+	n.scheduleCheck(now)
+}
+
+// removeCallback removes the given callback and returns true if it was active
+// Note: should be called while n.lock is held
+func (n *NodeBalance) removeCallback(id int) bool {
+	idx := n.callbackIndex[id]
+	if idx == -1 {
+		return false
+	}
+	n.callbackIndex[id] = -1
+	for i := idx; i < n.callbackCount-1; i++ {
+		n.callbackIndex[n.callbacks[i+1].id]--
+		n.callbacks[i] = n.callbacks[i+1]
+	}
+	n.callbackCount--
+	return true
+}
+
+// checkCallbacks checks whether the threshold of any of the active callbacks
+// have been reached and returns triggered callbacks.
+// Note: checkCallbacks assumes that the balance has been recently updated.
+func (n *NodeBalance) checkCallbacks(now mclock.AbsTime) (callbacks []func()) {
+	if n.callbackCount == 0 || n.capacity == 0 {
+		return
+	}
+	pri := n.balanceToPriority(n.balance, n.capacity)
+	for n.callbackCount != 0 && n.callbacks[n.callbackCount-1].threshold >= pri {
+		n.callbackCount--
+		n.callbackIndex[n.callbacks[n.callbackCount].id] = -1
+		callbacks = append(callbacks, n.callbacks[n.callbackCount].callback)
+	}
+	n.scheduleCheck(now)
+	return
+}
+
+// scheduleCheck sets up or updates a scheduled event to ensure that it will be called
+// again just after the next threshold has been reached.
+func (n *NodeBalance) scheduleCheck(now mclock.AbsTime) {
+	if n.callbackCount != 0 {
+		d, ok := n.timeUntil(n.callbacks[n.callbackCount-1].threshold)
+		if !ok {
+			n.nextUpdate = 0
+			n.updateAfter(0)
+			return
+		}
+		if n.nextUpdate == 0 || n.nextUpdate > now+mclock.AbsTime(d) {
+			if d > time.Second {
+				// Note: if the scheduled update is not in the very near future then we
+				// schedule the update a bit earlier. This way we do need to update a few
+				// extra times but don't need to reschedule every time a processed request
+				// brings the expected firing time a little bit closer.
+				d = ((d - time.Second) * 7 / 8) + time.Second
+			}
+			n.nextUpdate = now + mclock.AbsTime(d)
+			n.updateAfter(d)
+		}
+	} else {
+		n.nextUpdate = 0
+		n.updateAfter(0)
+	}
+}
+
+// updateAfter schedules a balance update and callback check in the future
+func (n *NodeBalance) updateAfter(dt time.Duration) {
+	if n.updateEvent == nil || n.updateEvent.Stop() {
+		if dt == 0 {
+			n.updateEvent = nil
+		} else {
+			n.updateEvent = n.bt.clock.AfterFunc(dt, func() {
+				var callbacks []func()
+				n.lock.Lock()
+				if n.callbackCount != 0 {
+					now := n.bt.clock.Now()
+					n.updateBalance(now)
+					callbacks = n.checkCallbacks(now)
+				}
+				n.lock.Unlock()
+				if callbacks != nil {
+					n.bt.ns.Operation(func() {
+						for _, cb := range callbacks {
+							cb()
+						}
+					})
+				}
+			})
+		}
+	}
+}
+
+// balanceExhausted should be called when the positive balance is exhausted (priority goes to zero/negative)
+// Note: this function should run inside a NodeStateMachine operation
+func (n *NodeBalance) balanceExhausted() {
+	n.lock.Lock()
+	n.storeBalance(true, false)
+	n.priority = false
+	n.lock.Unlock()
+	n.bt.ns.SetStateSub(n.node, nodestate.Flags{}, n.bt.PriorityFlag, 0)
+}
+
+// checkPriorityStatus checks whether the node has gained priority status and sets the priority
+// callback and flag if necessary. It assumes that the balance has been recently updated.
+// Note that the priority flag has to be set by the caller after the mutex has been released.
+func (n *NodeBalance) checkPriorityStatus() bool {
+	if !n.priority && !n.balance.pos.IsZero() {
+		n.priority = true
+		n.addCallback(balanceCallbackZero, 0, func() { n.balanceExhausted() })
+		return true
+	}
+	return false
+}
+
+// signalPriorityUpdate signals that the priority fell below the previous minimum estimate
+// Note: this function should run inside a NodeStateMachine operation
+func (n *NodeBalance) signalPriorityUpdate() {
+	n.bt.ns.SetStateSub(n.node, n.bt.UpdateFlag, nodestate.Flags{}, 0)
+	n.bt.ns.SetStateSub(n.node, nodestate.Flags{}, n.bt.UpdateFlag, 0)
+}
+
+// setCapacity updates the capacity value used for priority calculation
+// Note: capacity should never be zero
+// Note 2: this function should run inside a NodeStateMachine operation
+func (n *NodeBalance) setCapacity(capacity uint64) {
+	n.lock.Lock()
+	now := n.bt.clock.Now()
+	n.updateBalance(now)
+	n.capacity = capacity
+	callbacks := n.checkCallbacks(now)
+	n.lock.Unlock()
+	for _, cb := range callbacks {
+		cb()
+	}
+}
+
+// balanceToPriority converts a balance to a priority value. Lower priority means
+// first to disconnect. Positive balance translates to positive priority. If positive
+// balance is zero then negative balance translates to a negative priority.
+func (n *NodeBalance) balanceToPriority(b balance, capacity uint64) int64 {
+	if !b.pos.IsZero() {
+		return int64(b.pos.Value(n.bt.posExp.LogOffset(n.bt.clock.Now())) / capacity)
+	}
+	return -int64(b.neg.Value(n.bt.negExp.LogOffset(n.bt.clock.Now())))
+}
+
+// reducedBalance estimates the reduced balance at a given time in the fututre based
+// on the current balance, the time factor and an estimated average request cost per time ratio
+func (n *NodeBalance) reducedBalance(at mclock.AbsTime, capacity uint64, avgReqCost float64) balance {
+	dt := float64(at - n.lastUpdate)
+	b := n.balance
+	if !b.pos.IsZero() {
+		factor := n.posFactor.timePrice(capacity) + n.posFactor.RequestFactor*avgReqCost
+		diff := -int64(dt * factor)
+		dd := b.pos.Add(diff, n.bt.posExp.LogOffset(at))
+		if dd == diff {
+			dt = 0
+		} else {
+			dt += float64(dd) / factor
+		}
+	}
+	if dt > 0 {
+		factor := n.negFactor.timePrice(capacity) + n.negFactor.RequestFactor*avgReqCost
+		b.neg.Add(int64(dt*factor), n.bt.negExp.LogOffset(at))
+	}
+	return b
+}
+
+// timeUntil calculates the remaining time needed to reach a given priority level
+// assuming that no requests are processed until then. If the given level is never
+// reached then (0, false) is returned.
+// Note: the function assumes that the balance has been recently updated and
+// calculates the time starting from the last update.
+func (n *NodeBalance) timeUntil(priority int64) (time.Duration, bool) {
+	now := n.bt.clock.Now()
+	var dt float64
+	if !n.balance.pos.IsZero() {
+		posBalance := n.balance.pos.Value(n.bt.posExp.LogOffset(now))
+		timePrice := n.posFactor.timePrice(n.capacity)
+		if timePrice < 1e-100 {
+			return 0, false
+		}
+		if priority > 0 {
+			newBalance := uint64(priority) * n.capacity
+			if newBalance > posBalance {
+				return 0, false
+			}
+			dt = float64(posBalance-newBalance) / timePrice
+			return time.Duration(dt), true
+		} else {
+			dt = float64(posBalance) / timePrice
+		}
+	} else {
+		if priority > 0 {
+			return 0, false
+		}
+	}
+	// if we have a positive balance then dt equals the time needed to get it to zero
+	negBalance := n.balance.neg.Value(n.bt.negExp.LogOffset(now))
+	timePrice := n.negFactor.timePrice(n.capacity)
+	if uint64(-priority) > negBalance {
+		if timePrice < 1e-100 {
+			return 0, false
+		}
+		dt += float64(uint64(-priority)-negBalance) / timePrice
+	}
+	return time.Duration(dt), true
+}
diff --git a/les/lespay/server/balance_test.go b/les/lespay/server/balance_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..67e194437392c40e918499c7fece6b43b27b594d
--- /dev/null
+++ b/les/lespay/server/balance_test.go
@@ -0,0 +1,400 @@
+// Copyright 2019 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
+
+package server
+
+import (
+	"math/rand"
+	"reflect"
+	"testing"
+	"time"
+
+	"github.com/ethereum/go-ethereum/common/mclock"
+	"github.com/ethereum/go-ethereum/ethdb/memorydb"
+	"github.com/ethereum/go-ethereum/les/utils"
+	"github.com/ethereum/go-ethereum/p2p/enode"
+	"github.com/ethereum/go-ethereum/p2p/enr"
+	"github.com/ethereum/go-ethereum/p2p/nodestate"
+)
+
+var (
+	testFlag     = testSetup.NewFlag("testFlag")
+	connAddrFlag = testSetup.NewField("connAddr", reflect.TypeOf(""))
+	btTestSetup  = NewBalanceTrackerSetup(testSetup)
+)
+
+func init() {
+	btTestSetup.Connect(connAddrFlag, ppTestSetup.CapacityField)
+}
+
+type zeroExpirer struct{}
+
+func (z zeroExpirer) SetRate(now mclock.AbsTime, rate float64)                 {}
+func (z zeroExpirer) SetLogOffset(now mclock.AbsTime, logOffset utils.Fixed64) {}
+func (z zeroExpirer) LogOffset(now mclock.AbsTime) utils.Fixed64               { return 0 }
+
+type balanceTestSetup struct {
+	clock *mclock.Simulated
+	ns    *nodestate.NodeStateMachine
+	bt    *BalanceTracker
+}
+
+func newBalanceTestSetup() *balanceTestSetup {
+	clock := &mclock.Simulated{}
+	ns := nodestate.NewNodeStateMachine(nil, nil, clock, testSetup)
+	db := memorydb.New()
+	bt := NewBalanceTracker(ns, btTestSetup, db, clock, zeroExpirer{}, zeroExpirer{})
+	ns.Start()
+	return &balanceTestSetup{
+		clock: clock,
+		ns:    ns,
+		bt:    bt,
+	}
+}
+
+func (b *balanceTestSetup) newNode(capacity uint64) *NodeBalance {
+	node := enode.SignNull(&enr.Record{}, enode.ID{})
+	b.ns.SetState(node, testFlag, nodestate.Flags{}, 0)
+	b.ns.SetField(node, btTestSetup.connAddressField, "")
+	b.ns.SetField(node, ppTestSetup.CapacityField, capacity)
+	n, _ := b.ns.GetField(node, btTestSetup.BalanceField).(*NodeBalance)
+	return n
+}
+
+func (b *balanceTestSetup) stop() {
+	b.bt.Stop()
+	b.ns.Stop()
+}
+
+func TestAddBalance(t *testing.T) {
+	b := newBalanceTestSetup()
+	defer b.stop()
+
+	node := b.newNode(1000)
+	var inputs = []struct {
+		delta     int64
+		expect    [2]uint64
+		total     uint64
+		expectErr bool
+	}{
+		{100, [2]uint64{0, 100}, 100, false},
+		{-100, [2]uint64{100, 0}, 0, false},
+		{-100, [2]uint64{0, 0}, 0, false},
+		{1, [2]uint64{0, 1}, 1, false},
+		{maxBalance, [2]uint64{0, 0}, 0, true},
+	}
+	for _, i := range inputs {
+		old, new, err := node.AddBalance(i.delta)
+		if i.expectErr {
+			if err == nil {
+				t.Fatalf("Expect get error but nil")
+			}
+			continue
+		} else if err != nil {
+			t.Fatalf("Expect get no error but %v", err)
+		}
+		if old != i.expect[0] || new != i.expect[1] {
+			t.Fatalf("Positive balance mismatch, got %v -> %v", old, new)
+		}
+		if b.bt.TotalTokenAmount() != i.total {
+			t.Fatalf("Total positive balance mismatch, want %v, got %v", i.total, b.bt.TotalTokenAmount())
+		}
+	}
+}
+
+func TestSetBalance(t *testing.T) {
+	b := newBalanceTestSetup()
+	defer b.stop()
+	node := b.newNode(1000)
+
+	var inputs = []struct {
+		pos, neg uint64
+	}{
+		{1000, 0},
+		{0, 1000},
+		{1000, 1000},
+	}
+
+	for _, i := range inputs {
+		node.SetBalance(i.pos, i.neg)
+		pos, neg := node.GetBalance()
+		if pos != i.pos {
+			t.Fatalf("Positive balance mismatch, want %v, got %v", i.pos, pos)
+		}
+		if neg != i.neg {
+			t.Fatalf("Negative balance mismatch, want %v, got %v", i.neg, neg)
+		}
+	}
+}
+
+func TestBalanceTimeCost(t *testing.T) {
+	b := newBalanceTestSetup()
+	defer b.stop()
+	node := b.newNode(1000)
+
+	b.ns.SetField(node.node, ppTestSetup.CapacityField, uint64(1))
+	node.SetPriceFactors(PriceFactors{1, 0, 1}, PriceFactors{1, 0, 1})
+	node.SetBalance(uint64(time.Minute), 0) // 1 minute time allowance
+
+	var inputs = []struct {
+		runTime time.Duration
+		expPos  uint64
+		expNeg  uint64
+	}{
+		{time.Second, uint64(time.Second * 59), 0},
+		{0, uint64(time.Second * 59), 0},
+		{time.Second * 59, 0, 0},
+		{time.Second, 0, uint64(time.Second)},
+	}
+	for _, i := range inputs {
+		b.clock.Run(i.runTime)
+		if pos, _ := node.GetBalance(); pos != i.expPos {
+			t.Fatalf("Positive balance mismatch, want %v, got %v", i.expPos, pos)
+		}
+		if _, neg := node.GetBalance(); neg != i.expNeg {
+			t.Fatalf("Negative balance mismatch, want %v, got %v", i.expNeg, neg)
+		}
+	}
+
+	node.SetBalance(uint64(time.Minute), 0) // Refill 1 minute time allowance
+	for _, i := range inputs {
+		b.clock.Run(i.runTime)
+		if pos, _ := node.GetBalance(); pos != i.expPos {
+			t.Fatalf("Positive balance mismatch, want %v, got %v", i.expPos, pos)
+		}
+		if _, neg := node.GetBalance(); neg != i.expNeg {
+			t.Fatalf("Negative balance mismatch, want %v, got %v", i.expNeg, neg)
+		}
+	}
+}
+
+func TestBalanceReqCost(t *testing.T) {
+	b := newBalanceTestSetup()
+	defer b.stop()
+	node := b.newNode(1000)
+	node.SetPriceFactors(PriceFactors{1, 0, 1}, PriceFactors{1, 0, 1})
+
+	b.ns.SetField(node.node, ppTestSetup.CapacityField, uint64(1))
+	node.SetBalance(uint64(time.Minute), 0) // 1 minute time serving time allowance
+	var inputs = []struct {
+		reqCost uint64
+		expPos  uint64
+		expNeg  uint64
+	}{
+		{uint64(time.Second), uint64(time.Second * 59), 0},
+		{0, uint64(time.Second * 59), 0},
+		{uint64(time.Second * 59), 0, 0},
+		{uint64(time.Second), 0, uint64(time.Second)},
+	}
+	for _, i := range inputs {
+		node.RequestServed(i.reqCost)
+		if pos, _ := node.GetBalance(); pos != i.expPos {
+			t.Fatalf("Positive balance mismatch, want %v, got %v", i.expPos, pos)
+		}
+		if _, neg := node.GetBalance(); neg != i.expNeg {
+			t.Fatalf("Negative balance mismatch, want %v, got %v", i.expNeg, neg)
+		}
+	}
+}
+
+func TestBalanceToPriority(t *testing.T) {
+	b := newBalanceTestSetup()
+	defer b.stop()
+	node := b.newNode(1000)
+	node.SetPriceFactors(PriceFactors{1, 0, 1}, PriceFactors{1, 0, 1})
+
+	var inputs = []struct {
+		pos      uint64
+		neg      uint64
+		priority int64
+	}{
+		{1000, 0, 1},
+		{2000, 0, 2}, // Higher balance, higher priority value
+		{0, 0, 0},
+		{0, 1000, -1000},
+	}
+	for _, i := range inputs {
+		node.SetBalance(i.pos, i.neg)
+		priority := node.Priority(b.clock.Now(), 1000)
+		if priority != i.priority {
+			t.Fatalf("Priority mismatch, want %v, got %v", i.priority, priority)
+		}
+	}
+}
+
+func TestEstimatedPriority(t *testing.T) {
+	b := newBalanceTestSetup()
+	defer b.stop()
+	node := b.newNode(1000000000)
+	node.SetPriceFactors(PriceFactors{1, 0, 1}, PriceFactors{1, 0, 1})
+
+	b.ns.SetField(node.node, ppTestSetup.CapacityField, uint64(1))
+	node.SetBalance(uint64(time.Minute), 0)
+	var inputs = []struct {
+		runTime    time.Duration // time cost
+		futureTime time.Duration // diff of future time
+		reqCost    uint64        // single request cost
+		priority   int64         // expected estimated priority
+	}{
+		{time.Second, time.Second, 0, 58},
+		{0, time.Second, 0, 58},
+
+		// 2 seconds time cost, 1 second estimated time cost, 10^9 request cost,
+		// 10^9 estimated request cost per second.
+		{time.Second, time.Second, 1000000000, 55},
+
+		// 3 seconds time cost, 3 second estimated time cost, 10^9*2 request cost,
+		// 4*10^9 estimated request cost.
+		{time.Second, 3 * time.Second, 1000000000, 48},
+
+		// All positive balance is used up
+		{time.Second * 55, 0, 0, 0},
+
+		// 1 minute estimated time cost, 4/58 * 10^9 estimated request cost per sec.
+		{0, time.Minute, 0, -int64(time.Minute) - int64(time.Second)*120/29},
+	}
+	for _, i := range inputs {
+		b.clock.Run(i.runTime)
+		node.RequestServed(i.reqCost)
+		priority := node.EstMinPriority(b.clock.Now()+mclock.AbsTime(i.futureTime), 1000000000, false)
+		if priority != i.priority {
+			t.Fatalf("Estimated priority mismatch, want %v, got %v", i.priority, priority)
+		}
+	}
+}
+
+func TestPosBalanceMissing(t *testing.T) {
+	b := newBalanceTestSetup()
+	defer b.stop()
+	node := b.newNode(1000)
+	node.SetPriceFactors(PriceFactors{1, 0, 1}, PriceFactors{1, 0, 1})
+
+	b.ns.SetField(node.node, ppTestSetup.CapacityField, uint64(1))
+	var inputs = []struct {
+		pos, neg uint64
+		priority int64
+		cap      uint64
+		after    time.Duration
+		expect   uint64
+	}{
+		{uint64(time.Second * 2), 0, 0, 1, time.Second, 0},
+		{uint64(time.Second * 2), 0, 0, 1, 2 * time.Second, 1},
+		{uint64(time.Second * 2), 0, int64(time.Second), 1, 2 * time.Second, uint64(time.Second) + 1},
+		{0, 0, int64(time.Second), 1, time.Second, uint64(2*time.Second) + 1},
+		{0, 0, -int64(time.Second), 1, time.Second, 1},
+	}
+	for _, i := range inputs {
+		node.SetBalance(i.pos, i.neg)
+		got := node.PosBalanceMissing(i.priority, i.cap, i.after)
+		if got != i.expect {
+			t.Fatalf("Missing budget mismatch, want %v, got %v", i.expect, got)
+		}
+	}
+}
+
+func TestPostiveBalanceCounting(t *testing.T) {
+	b := newBalanceTestSetup()
+	defer b.stop()
+
+	var nodes []*NodeBalance
+	for i := 0; i < 100; i += 1 {
+		node := b.newNode(1000000)
+		node.SetPriceFactors(PriceFactors{1, 0, 1}, PriceFactors{1, 0, 1})
+		nodes = append(nodes, node)
+	}
+
+	// Allocate service token
+	var sum uint64
+	for i := 0; i < 100; i += 1 {
+		amount := int64(rand.Intn(100) + 100)
+		nodes[i].AddBalance(amount)
+		sum += uint64(amount)
+	}
+	if b.bt.TotalTokenAmount() != sum {
+		t.Fatalf("Invalid token amount")
+	}
+
+	// Change client status
+	for i := 0; i < 100; i += 1 {
+		if rand.Intn(2) == 0 {
+			b.ns.SetField(nodes[i].node, ppTestSetup.CapacityField, uint64(1))
+		}
+	}
+	if b.bt.TotalTokenAmount() != sum {
+		t.Fatalf("Invalid token amount")
+	}
+	for i := 0; i < 100; i += 1 {
+		if rand.Intn(2) == 0 {
+			b.ns.SetField(nodes[i].node, ppTestSetup.CapacityField, uint64(1))
+		}
+	}
+	if b.bt.TotalTokenAmount() != sum {
+		t.Fatalf("Invalid token amount")
+	}
+}
+
+func TestCallbackChecking(t *testing.T) {
+	b := newBalanceTestSetup()
+	defer b.stop()
+	node := b.newNode(1000000)
+	node.SetPriceFactors(PriceFactors{1, 0, 1}, PriceFactors{1, 0, 1})
+
+	var inputs = []struct {
+		priority int64
+		expDiff  time.Duration
+	}{
+		{500, time.Millisecond * 500},
+		{0, time.Second},
+		{-int64(time.Second), 2 * time.Second},
+	}
+	node.SetBalance(uint64(time.Second), 0)
+	for _, i := range inputs {
+		diff, _ := node.timeUntil(i.priority)
+		if diff != i.expDiff {
+			t.Fatalf("Time difference mismatch, want %v, got %v", i.expDiff, diff)
+		}
+	}
+}
+
+func TestCallback(t *testing.T) {
+	b := newBalanceTestSetup()
+	defer b.stop()
+	node := b.newNode(1000)
+	node.SetPriceFactors(PriceFactors{1, 0, 1}, PriceFactors{1, 0, 1})
+	b.ns.SetField(node.node, ppTestSetup.CapacityField, uint64(1))
+
+	callCh := make(chan struct{}, 1)
+	node.SetBalance(uint64(time.Minute), 0)
+	node.addCallback(balanceCallbackZero, 0, func() { callCh <- struct{}{} })
+
+	b.clock.Run(time.Minute)
+	select {
+	case <-callCh:
+	case <-time.NewTimer(time.Second).C:
+		t.Fatalf("Callback hasn't been called yet")
+	}
+
+	node.SetBalance(uint64(time.Minute), 0)
+	node.addCallback(balanceCallbackZero, 0, func() { callCh <- struct{}{} })
+	node.removeCallback(balanceCallbackZero)
+
+	b.clock.Run(time.Minute)
+	select {
+	case <-callCh:
+		t.Fatalf("Callback shouldn't be called")
+	case <-time.NewTimer(time.Millisecond * 100).C:
+	}
+}
diff --git a/les/lespay/server/balance_tracker.go b/les/lespay/server/balance_tracker.go
new file mode 100644
index 0000000000000000000000000000000000000000..c1ea3c649671ceebe8e81dddc40c34f2b5032021
--- /dev/null
+++ b/les/lespay/server/balance_tracker.go
@@ -0,0 +1,291 @@
+// Copyright 2020 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
+
+package server
+
+import (
+	"reflect"
+	"sync"
+	"time"
+
+	"github.com/ethereum/go-ethereum/common/mclock"
+	"github.com/ethereum/go-ethereum/ethdb"
+	"github.com/ethereum/go-ethereum/les/utils"
+	"github.com/ethereum/go-ethereum/p2p/enode"
+	"github.com/ethereum/go-ethereum/p2p/nodestate"
+)
+
+const (
+	posThreshold             = 1000000         // minimum positive balance that is persisted in the database
+	negThreshold             = 1000000         // minimum negative balance that is persisted in the database
+	persistExpirationRefresh = time.Minute * 5 // refresh period of the token expiration persistence
+)
+
+// BalanceTrackerSetup contains node state flags and fields used by BalanceTracker
+type BalanceTrackerSetup struct {
+	// controlled by PriorityPool
+	PriorityFlag, UpdateFlag nodestate.Flags
+	BalanceField             nodestate.Field
+	// external connections
+	connAddressField, capacityField nodestate.Field
+}
+
+// NewBalanceTrackerSetup creates a new BalanceTrackerSetup and initializes the fields
+// and flags controlled by BalanceTracker
+func NewBalanceTrackerSetup(setup *nodestate.Setup) BalanceTrackerSetup {
+	return BalanceTrackerSetup{
+		// PriorityFlag is set if the node has a positive balance
+		PriorityFlag: setup.NewFlag("priorityNode"),
+		// UpdateFlag set and then immediately reset if the balance has been updated and
+		// therefore priority is suddenly changed
+		UpdateFlag: setup.NewFlag("balanceUpdate"),
+		// BalanceField contains the NodeBalance struct which implements nodePriority,
+		// allowing on-demand priority calculation and future priority estimation
+		BalanceField: setup.NewField("balance", reflect.TypeOf(&NodeBalance{})),
+	}
+}
+
+// Connect sets the fields used by BalanceTracker as an input
+func (bts *BalanceTrackerSetup) Connect(connAddressField, capacityField nodestate.Field) {
+	bts.connAddressField = connAddressField
+	bts.capacityField = capacityField
+}
+
+// BalanceTracker tracks positive and negative balances for connected nodes.
+// After connAddressField is set externally, a NodeBalance is created and previous
+// balance values are loaded from the database. Both balances are exponentially expired
+// values. Costs are deducted from the positive balance if present, otherwise added to
+// the negative balance. If the capacity is non-zero then a time cost is applied
+// continuously while individual request costs are applied immediately.
+// The two balances are translated into a single priority value that also depends
+// on the actual capacity.
+type BalanceTracker struct {
+	BalanceTrackerSetup
+	clock              mclock.Clock
+	lock               sync.Mutex
+	ns                 *nodestate.NodeStateMachine
+	ndb                *nodeDB
+	posExp, negExp     utils.ValueExpirer
+	posExpTC, negExpTC uint64
+
+	active, inactive utils.ExpiredValue
+	balanceTimer     *utils.UpdateTimer
+	quit             chan struct{}
+}
+
+// NewBalanceTracker creates a new BalanceTracker
+func NewBalanceTracker(ns *nodestate.NodeStateMachine, setup BalanceTrackerSetup, db ethdb.KeyValueStore, clock mclock.Clock, posExp, negExp utils.ValueExpirer) *BalanceTracker {
+	ndb := newNodeDB(db, clock)
+	bt := &BalanceTracker{
+		ns:                  ns,
+		BalanceTrackerSetup: setup,
+		ndb:                 ndb,
+		clock:               clock,
+		posExp:              posExp,
+		negExp:              negExp,
+		balanceTimer:        utils.NewUpdateTimer(clock, time.Second*10),
+		quit:                make(chan struct{}),
+	}
+	bt.ndb.forEachBalance(false, func(id enode.ID, balance utils.ExpiredValue) bool {
+		bt.inactive.AddExp(balance)
+		return true
+	})
+
+	ns.SubscribeField(bt.capacityField, func(node *enode.Node, state nodestate.Flags, oldValue, newValue interface{}) {
+		n, _ := ns.GetField(node, bt.BalanceField).(*NodeBalance)
+		if n == nil {
+			return
+		}
+
+		ov, _ := oldValue.(uint64)
+		nv, _ := newValue.(uint64)
+		if ov == 0 && nv != 0 {
+			n.activate()
+		}
+		if nv != 0 {
+			n.setCapacity(nv)
+		}
+		if ov != 0 && nv == 0 {
+			n.deactivate()
+		}
+	})
+	ns.SubscribeField(bt.connAddressField, func(node *enode.Node, state nodestate.Flags, oldValue, newValue interface{}) {
+		if newValue != nil {
+			ns.SetFieldSub(node, bt.BalanceField, bt.newNodeBalance(node, newValue.(string)))
+		} else {
+			ns.SetStateSub(node, nodestate.Flags{}, bt.PriorityFlag, 0)
+			if b, _ := ns.GetField(node, bt.BalanceField).(*NodeBalance); b != nil {
+				b.deactivate()
+			}
+			ns.SetFieldSub(node, bt.BalanceField, nil)
+		}
+	})
+
+	// The positive and negative balances of clients are stored in database
+	// and both of these decay exponentially over time. Delete them if the
+	// value is small enough.
+	bt.ndb.evictCallBack = bt.canDropBalance
+
+	go func() {
+		for {
+			select {
+			case <-clock.After(persistExpirationRefresh):
+				now := clock.Now()
+				bt.ndb.setExpiration(posExp.LogOffset(now), negExp.LogOffset(now))
+			case <-bt.quit:
+				return
+			}
+		}
+	}()
+	return bt
+}
+
+// Stop saves expiration offset and unsaved node balances and shuts BalanceTracker down
+func (bt *BalanceTracker) Stop() {
+	now := bt.clock.Now()
+	bt.ndb.setExpiration(bt.posExp.LogOffset(now), bt.negExp.LogOffset(now))
+	close(bt.quit)
+	bt.ns.ForEach(nodestate.Flags{}, nodestate.Flags{}, func(node *enode.Node, state nodestate.Flags) {
+		if n, ok := bt.ns.GetField(node, bt.BalanceField).(*NodeBalance); ok {
+			n.lock.Lock()
+			n.storeBalance(true, true)
+			n.lock.Unlock()
+			bt.ns.SetField(node, bt.BalanceField, nil)
+		}
+	})
+	bt.ndb.close()
+}
+
+// TotalTokenAmount returns the current total amount of service tokens in existence
+func (bt *BalanceTracker) TotalTokenAmount() uint64 {
+	bt.lock.Lock()
+	defer bt.lock.Unlock()
+
+	bt.balanceTimer.Update(func(_ time.Duration) bool {
+		bt.active = utils.ExpiredValue{}
+		bt.ns.ForEach(nodestate.Flags{}, nodestate.Flags{}, func(node *enode.Node, state nodestate.Flags) {
+			if n, ok := bt.ns.GetField(node, bt.BalanceField).(*NodeBalance); ok {
+				pos, _ := n.GetRawBalance()
+				bt.active.AddExp(pos)
+			}
+		})
+		return true
+	})
+	total := bt.active
+	total.AddExp(bt.inactive)
+	return total.Value(bt.posExp.LogOffset(bt.clock.Now()))
+}
+
+// GetPosBalanceIDs lists node IDs with an associated positive balance
+func (bt *BalanceTracker) GetPosBalanceIDs(start, stop enode.ID, maxCount int) (result []enode.ID) {
+	return bt.ndb.getPosBalanceIDs(start, stop, maxCount)
+}
+
+// SetExpirationTCs sets positive and negative token expiration time constants.
+// Specified in seconds, 0 means infinite (no expiration).
+func (bt *BalanceTracker) SetExpirationTCs(pos, neg uint64) {
+	bt.lock.Lock()
+	defer bt.lock.Unlock()
+
+	bt.posExpTC, bt.negExpTC = pos, neg
+	now := bt.clock.Now()
+	if pos > 0 {
+		bt.posExp.SetRate(now, 1/float64(pos*uint64(time.Second)))
+	} else {
+		bt.posExp.SetRate(now, 0)
+	}
+	if neg > 0 {
+		bt.negExp.SetRate(now, 1/float64(neg*uint64(time.Second)))
+	} else {
+		bt.negExp.SetRate(now, 0)
+	}
+}
+
+// GetExpirationTCs returns the current positive and negative token expiration
+// time constants
+func (bt *BalanceTracker) GetExpirationTCs() (pos, neg uint64) {
+	bt.lock.Lock()
+	defer bt.lock.Unlock()
+
+	return bt.posExpTC, bt.negExpTC
+}
+
+// newNodeBalance loads balances from the database and creates a NodeBalance instance
+// for the given node. It also sets the PriorityFlag and adds balanceCallbackZero if
+// the node has a positive balance.
+// Note: this function should run inside a NodeStateMachine operation
+func (bt *BalanceTracker) newNodeBalance(node *enode.Node, negBalanceKey string) *NodeBalance {
+	pb := bt.ndb.getOrNewBalance(node.ID().Bytes(), false)
+	nb := bt.ndb.getOrNewBalance([]byte(negBalanceKey), true)
+	n := &NodeBalance{
+		bt:          bt,
+		node:        node,
+		connAddress: negBalanceKey,
+		balance:     balance{pos: pb, neg: nb},
+		initTime:    bt.clock.Now(),
+		lastUpdate:  bt.clock.Now(),
+	}
+	for i := range n.callbackIndex {
+		n.callbackIndex[i] = -1
+	}
+	if n.checkPriorityStatus() {
+		n.bt.ns.SetStateSub(n.node, n.bt.PriorityFlag, nodestate.Flags{}, 0)
+	}
+	return n
+}
+
+// storeBalance stores either a positive or a negative balance in the database
+func (bt *BalanceTracker) storeBalance(id []byte, neg bool, value utils.ExpiredValue) {
+	if bt.canDropBalance(bt.clock.Now(), neg, value) {
+		bt.ndb.delBalance(id, neg) // balance is small enough, drop it directly.
+	} else {
+		bt.ndb.setBalance(id, neg, value)
+	}
+}
+
+// canDropBalance tells whether a positive or negative balance is below the threshold
+// and therefore can be dropped from the database
+func (bt *BalanceTracker) canDropBalance(now mclock.AbsTime, neg bool, b utils.ExpiredValue) bool {
+	if neg {
+		return b.Value(bt.negExp.LogOffset(now)) <= negThreshold
+	} else {
+		return b.Value(bt.posExp.LogOffset(now)) <= posThreshold
+	}
+}
+
+// updateTotalBalance adjusts the total balance after executing given callback.
+func (bt *BalanceTracker) updateTotalBalance(n *NodeBalance, callback func() bool) {
+	bt.lock.Lock()
+	defer bt.lock.Unlock()
+
+	n.lock.Lock()
+	defer n.lock.Unlock()
+
+	original, active := n.balance.pos, n.active
+	if !callback() {
+		return
+	}
+	if active {
+		bt.active.SubExp(original)
+	} else {
+		bt.inactive.SubExp(original)
+	}
+	if n.active {
+		bt.active.AddExp(n.balance.pos)
+	} else {
+		bt.inactive.AddExp(n.balance.pos)
+	}
+}
diff --git a/les/lespay/server/clientdb.go b/les/lespay/server/clientdb.go
new file mode 100644
index 0000000000000000000000000000000000000000..c23f1e3b2f3f8f3e5e5a24c713485412db7af1f1
--- /dev/null
+++ b/les/lespay/server/clientdb.go
@@ -0,0 +1,245 @@
+// Copyright 2020 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
+
+package server
+
+import (
+	"bytes"
+	"encoding/binary"
+	"time"
+
+	"github.com/ethereum/go-ethereum/common"
+	"github.com/ethereum/go-ethereum/common/mclock"
+	"github.com/ethereum/go-ethereum/ethdb"
+	"github.com/ethereum/go-ethereum/les/utils"
+	"github.com/ethereum/go-ethereum/log"
+	"github.com/ethereum/go-ethereum/p2p/enode"
+	"github.com/ethereum/go-ethereum/rlp"
+	lru "github.com/hashicorp/golang-lru"
+)
+
+const (
+	balanceCacheLimit = 8192 // the maximum number of cached items in service token balance queue
+
+	// nodeDBVersion is the version identifier of the node data in db
+	//
+	// Changelog:
+	// * Replace `lastTotal` with `meta` in positive balance: version 0=>1
+	nodeDBVersion = 1
+
+	// dbCleanupCycle is the cycle of db for useless data cleanup
+	dbCleanupCycle = time.Hour
+)
+
+var (
+	positiveBalancePrefix = []byte("pb:")         // dbVersion(uint16 big endian) + positiveBalancePrefix + id -> balance
+	negativeBalancePrefix = []byte("nb:")         // dbVersion(uint16 big endian) + negativeBalancePrefix + ip -> balance
+	expirationKey         = []byte("expiration:") // dbVersion(uint16 big endian) + expirationKey -> posExp, negExp
+)
+
+type nodeDB struct {
+	db            ethdb.KeyValueStore
+	cache         *lru.Cache
+	auxbuf        []byte                                              // 37-byte auxiliary buffer for key encoding
+	verbuf        [2]byte                                             // 2-byte auxiliary buffer for db version
+	evictCallBack func(mclock.AbsTime, bool, utils.ExpiredValue) bool // Callback to determine whether the balance can be evicted.
+	clock         mclock.Clock
+	closeCh       chan struct{}
+	cleanupHook   func() // Test hook used for testing
+}
+
+func newNodeDB(db ethdb.KeyValueStore, clock mclock.Clock) *nodeDB {
+	cache, _ := lru.New(balanceCacheLimit)
+	ndb := &nodeDB{
+		db:      db,
+		cache:   cache,
+		auxbuf:  make([]byte, 37),
+		clock:   clock,
+		closeCh: make(chan struct{}),
+	}
+	binary.BigEndian.PutUint16(ndb.verbuf[:], uint16(nodeDBVersion))
+	go ndb.expirer()
+	return ndb
+}
+
+func (db *nodeDB) close() {
+	close(db.closeCh)
+}
+
+func (db *nodeDB) getPrefix(neg bool) []byte {
+	prefix := positiveBalancePrefix
+	if neg {
+		prefix = negativeBalancePrefix
+	}
+	return append(db.verbuf[:], prefix...)
+}
+
+func (db *nodeDB) key(id []byte, neg bool) []byte {
+	prefix := positiveBalancePrefix
+	if neg {
+		prefix = negativeBalancePrefix
+	}
+	if len(prefix)+len(db.verbuf)+len(id) > len(db.auxbuf) {
+		db.auxbuf = append(db.auxbuf, make([]byte, len(prefix)+len(db.verbuf)+len(id)-len(db.auxbuf))...)
+	}
+	copy(db.auxbuf[:len(db.verbuf)], db.verbuf[:])
+	copy(db.auxbuf[len(db.verbuf):len(db.verbuf)+len(prefix)], prefix)
+	copy(db.auxbuf[len(prefix)+len(db.verbuf):len(prefix)+len(db.verbuf)+len(id)], id)
+	return db.auxbuf[:len(prefix)+len(db.verbuf)+len(id)]
+}
+
+func (db *nodeDB) getExpiration() (utils.Fixed64, utils.Fixed64) {
+	blob, err := db.db.Get(append(db.verbuf[:], expirationKey...))
+	if err != nil || len(blob) != 16 {
+		return 0, 0
+	}
+	return utils.Fixed64(binary.BigEndian.Uint64(blob[:8])), utils.Fixed64(binary.BigEndian.Uint64(blob[8:16]))
+}
+
+func (db *nodeDB) setExpiration(pos, neg utils.Fixed64) {
+	var buff [16]byte
+	binary.BigEndian.PutUint64(buff[:8], uint64(pos))
+	binary.BigEndian.PutUint64(buff[8:16], uint64(neg))
+	db.db.Put(append(db.verbuf[:], expirationKey...), buff[:16])
+}
+
+func (db *nodeDB) getOrNewBalance(id []byte, neg bool) utils.ExpiredValue {
+	key := db.key(id, neg)
+	item, exist := db.cache.Get(string(key))
+	if exist {
+		return item.(utils.ExpiredValue)
+	}
+	var b utils.ExpiredValue
+	enc, err := db.db.Get(key)
+	if err != nil || len(enc) == 0 {
+		return b
+	}
+	if err := rlp.DecodeBytes(enc, &b); err != nil {
+		log.Crit("Failed to decode positive balance", "err", err)
+	}
+	db.cache.Add(string(key), b)
+	return b
+}
+
+func (db *nodeDB) setBalance(id []byte, neg bool, b utils.ExpiredValue) {
+	key := db.key(id, neg)
+	enc, err := rlp.EncodeToBytes(&(b))
+	if err != nil {
+		log.Crit("Failed to encode positive balance", "err", err)
+	}
+	db.db.Put(key, enc)
+	db.cache.Add(string(key), b)
+}
+
+func (db *nodeDB) delBalance(id []byte, neg bool) {
+	key := db.key(id, neg)
+	db.db.Delete(key)
+	db.cache.Remove(string(key))
+}
+
+// getPosBalanceIDs returns a lexicographically ordered list of IDs of accounts
+// with a positive balance
+func (db *nodeDB) getPosBalanceIDs(start, stop enode.ID, maxCount int) (result []enode.ID) {
+	if maxCount <= 0 {
+		return
+	}
+	prefix := db.getPrefix(false)
+	keylen := len(prefix) + len(enode.ID{})
+
+	it := db.db.NewIterator(prefix, start.Bytes())
+	defer it.Release()
+
+	for it.Next() {
+		var id enode.ID
+		if len(it.Key()) != keylen {
+			return
+		}
+		copy(id[:], it.Key()[keylen-len(id):])
+		if bytes.Compare(id.Bytes(), stop.Bytes()) >= 0 {
+			return
+		}
+		result = append(result, id)
+		if len(result) == maxCount {
+			return
+		}
+	}
+	return
+}
+
+// forEachBalance iterates all balances and passes values to callback.
+func (db *nodeDB) forEachBalance(neg bool, callback func(id enode.ID, balance utils.ExpiredValue) bool) {
+	prefix := db.getPrefix(neg)
+	keylen := len(prefix) + len(enode.ID{})
+
+	it := db.db.NewIterator(prefix, nil)
+	defer it.Release()
+
+	for it.Next() {
+		var id enode.ID
+		if len(it.Key()) != keylen {
+			return
+		}
+		copy(id[:], it.Key()[keylen-len(id):])
+
+		var b utils.ExpiredValue
+		if err := rlp.DecodeBytes(it.Value(), &b); err != nil {
+			continue
+		}
+		if !callback(id, b) {
+			return
+		}
+	}
+}
+
+func (db *nodeDB) expirer() {
+	for {
+		select {
+		case <-db.clock.After(dbCleanupCycle):
+			db.expireNodes()
+		case <-db.closeCh:
+			return
+		}
+	}
+}
+
+// expireNodes iterates the whole node db and checks whether the
+// token balances can be deleted.
+func (db *nodeDB) expireNodes() {
+	var (
+		visited int
+		deleted int
+		start   = time.Now()
+	)
+	for _, neg := range []bool{false, true} {
+		iter := db.db.NewIterator(db.getPrefix(neg), nil)
+		for iter.Next() {
+			visited++
+			var balance utils.ExpiredValue
+			if err := rlp.DecodeBytes(iter.Value(), &balance); err != nil {
+				log.Crit("Failed to decode negative balance", "err", err)
+			}
+			if db.evictCallBack != nil && db.evictCallBack(db.clock.Now(), neg, balance) {
+				deleted++
+				db.db.Delete(iter.Key())
+			}
+		}
+	}
+	// Invoke testing hook if it's not nil.
+	if db.cleanupHook != nil {
+		db.cleanupHook()
+	}
+	log.Debug("Expire nodes", "visited", visited, "deleted", deleted, "elapsed", common.PrettyDuration(time.Since(start)))
+}
diff --git a/les/lespay/server/clientdb_test.go b/les/lespay/server/clientdb_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..353d84aead47fc41f84a8550f9efd77eff28d207
--- /dev/null
+++ b/les/lespay/server/clientdb_test.go
@@ -0,0 +1,144 @@
+// Copyright 2020 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
+
+package server
+
+import (
+	"reflect"
+	"testing"
+	"time"
+
+	"github.com/ethereum/go-ethereum/common/mclock"
+	"github.com/ethereum/go-ethereum/core/rawdb"
+	"github.com/ethereum/go-ethereum/les/utils"
+	"github.com/ethereum/go-ethereum/p2p/enode"
+)
+
+func expval(v uint64) utils.ExpiredValue {
+	return utils.ExpiredValue{Base: v}
+}
+
+func TestNodeDB(t *testing.T) {
+	ndb := newNodeDB(rawdb.NewMemoryDatabase(), mclock.System{})
+	defer ndb.close()
+
+	var cases = []struct {
+		id       enode.ID
+		ip       string
+		balance  utils.ExpiredValue
+		positive bool
+	}{
+		{enode.ID{0x00, 0x01, 0x02}, "", expval(100), true},
+		{enode.ID{0x00, 0x01, 0x02}, "", expval(200), true},
+		{enode.ID{}, "127.0.0.1", expval(100), false},
+		{enode.ID{}, "127.0.0.1", expval(200), false},
+	}
+	for _, c := range cases {
+		if c.positive {
+			ndb.setBalance(c.id.Bytes(), false, c.balance)
+			if pb := ndb.getOrNewBalance(c.id.Bytes(), false); !reflect.DeepEqual(pb, c.balance) {
+				t.Fatalf("Positive balance mismatch, want %v, got %v", c.balance, pb)
+			}
+		} else {
+			ndb.setBalance([]byte(c.ip), true, c.balance)
+			if nb := ndb.getOrNewBalance([]byte(c.ip), true); !reflect.DeepEqual(nb, c.balance) {
+				t.Fatalf("Negative balance mismatch, want %v, got %v", c.balance, nb)
+			}
+		}
+	}
+	for _, c := range cases {
+		if c.positive {
+			ndb.delBalance(c.id.Bytes(), false)
+			if pb := ndb.getOrNewBalance(c.id.Bytes(), false); !reflect.DeepEqual(pb, utils.ExpiredValue{}) {
+				t.Fatalf("Positive balance mismatch, want %v, got %v", utils.ExpiredValue{}, pb)
+			}
+		} else {
+			ndb.delBalance([]byte(c.ip), true)
+			if nb := ndb.getOrNewBalance([]byte(c.ip), true); !reflect.DeepEqual(nb, utils.ExpiredValue{}) {
+				t.Fatalf("Negative balance mismatch, want %v, got %v", utils.ExpiredValue{}, nb)
+			}
+		}
+	}
+	posExp, negExp := utils.Fixed64(1000), utils.Fixed64(2000)
+	ndb.setExpiration(posExp, negExp)
+	if pos, neg := ndb.getExpiration(); pos != posExp || neg != negExp {
+		t.Fatalf("Expiration mismatch, want %v / %v, got %v / %v", posExp, negExp, pos, neg)
+	}
+	/*	curBalance := currencyBalance{typ: "ETH", amount: 10000}
+		ndb.setCurrencyBalance(enode.ID{0x01, 0x02}, curBalance)
+		if got := ndb.getCurrencyBalance(enode.ID{0x01, 0x02}); !reflect.DeepEqual(got, curBalance) {
+			t.Fatalf("Currency balance mismatch, want %v, got %v", curBalance, got)
+		}*/
+}
+
+func TestNodeDBExpiration(t *testing.T) {
+	var (
+		iterated int
+		done     = make(chan struct{}, 1)
+	)
+	callback := func(now mclock.AbsTime, neg bool, b utils.ExpiredValue) bool {
+		iterated += 1
+		return true
+	}
+	clock := &mclock.Simulated{}
+	ndb := newNodeDB(rawdb.NewMemoryDatabase(), clock)
+	defer ndb.close()
+	ndb.evictCallBack = callback
+	ndb.cleanupHook = func() { done <- struct{}{} }
+
+	var cases = []struct {
+		id      []byte
+		neg     bool
+		balance utils.ExpiredValue
+	}{
+		{[]byte{0x01, 0x02}, false, expval(1)},
+		{[]byte{0x03, 0x04}, false, expval(1)},
+		{[]byte{0x05, 0x06}, false, expval(1)},
+		{[]byte{0x07, 0x08}, false, expval(1)},
+
+		{[]byte("127.0.0.1"), true, expval(1)},
+		{[]byte("127.0.0.2"), true, expval(1)},
+		{[]byte("127.0.0.3"), true, expval(1)},
+		{[]byte("127.0.0.4"), true, expval(1)},
+	}
+	for _, c := range cases {
+		ndb.setBalance(c.id, c.neg, c.balance)
+	}
+	clock.WaitForTimers(1)
+	clock.Run(time.Hour + time.Minute)
+	select {
+	case <-done:
+	case <-time.NewTimer(time.Second).C:
+		t.Fatalf("timeout")
+	}
+	if iterated != 8 {
+		t.Fatalf("Failed to evict useless balances, want %v, got %d", 8, iterated)
+	}
+
+	for _, c := range cases {
+		ndb.setBalance(c.id, c.neg, c.balance)
+	}
+	clock.WaitForTimers(1)
+	clock.Run(time.Hour + time.Minute)
+	select {
+	case <-done:
+	case <-time.NewTimer(time.Second).C:
+		t.Fatalf("timeout")
+	}
+	if iterated != 16 {
+		t.Fatalf("Failed to evict useless balances, want %v, got %d", 16, iterated)
+	}
+}
diff --git a/les/lespay/server/prioritypool.go b/les/lespay/server/prioritypool.go
new file mode 100644
index 0000000000000000000000000000000000000000..52224e093e931c13e3ef69211d26794473c1874c
--- /dev/null
+++ b/les/lespay/server/prioritypool.go
@@ -0,0 +1,503 @@
+// Copyright 2020 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
+
+package server
+
+import (
+	"math"
+	"reflect"
+	"sync"
+	"time"
+
+	"github.com/ethereum/go-ethereum/common/mclock"
+	"github.com/ethereum/go-ethereum/common/prque"
+	"github.com/ethereum/go-ethereum/log"
+	"github.com/ethereum/go-ethereum/p2p/enode"
+	"github.com/ethereum/go-ethereum/p2p/nodestate"
+)
+
+const (
+	lazyQueueRefresh = time.Second * 10 // refresh period of the active queue
+)
+
+// PriorityPoolSetup contains node state flags and fields used by PriorityPool
+// Note: ActiveFlag and InactiveFlag can be controlled both externally and by the pool,
+// see PriorityPool description for details.
+type PriorityPoolSetup struct {
+	// controlled by PriorityPool
+	ActiveFlag, InactiveFlag       nodestate.Flags
+	CapacityField, ppNodeInfoField nodestate.Field
+	// external connections
+	updateFlag    nodestate.Flags
+	priorityField nodestate.Field
+}
+
+// NewPriorityPoolSetup creates a new PriorityPoolSetup and initializes the fields
+// and flags controlled by PriorityPool
+func NewPriorityPoolSetup(setup *nodestate.Setup) PriorityPoolSetup {
+	return PriorityPoolSetup{
+		ActiveFlag:      setup.NewFlag("active"),
+		InactiveFlag:    setup.NewFlag("inactive"),
+		CapacityField:   setup.NewField("capacity", reflect.TypeOf(uint64(0))),
+		ppNodeInfoField: setup.NewField("ppNodeInfo", reflect.TypeOf(&ppNodeInfo{})),
+	}
+}
+
+// Connect sets the fields and flags used by PriorityPool as an input
+func (pps *PriorityPoolSetup) Connect(priorityField nodestate.Field, updateFlag nodestate.Flags) {
+	pps.priorityField = priorityField // should implement nodePriority
+	pps.updateFlag = updateFlag       // triggers an immediate priority update
+}
+
+// PriorityPool handles a set of nodes where each node has a capacity (a scalar value)
+// and a priority (which can change over time and can also depend on the capacity).
+// A node is active if it has at least the necessary minimal amount of capacity while
+// inactive nodes have 0 capacity (values between 0 and the minimum are not allowed).
+// The pool ensures that the number and total capacity of all active nodes are limited
+// and the highest priority nodes are active at all times (limits can be changed
+// during operation with immediate effect).
+//
+// When activating clients a priority bias is applied in favor of the already active
+// nodes in order to avoid nodes quickly alternating between active and inactive states
+// when their priorities are close to each other. The bias is specified in terms of
+// duration (time) because priorities are expected to usually get lower over time and
+// therefore a future minimum prediction (see EstMinPriority) should monotonously
+// decrease with the specified time parameter.
+// This time bias can be interpreted as minimum expected active time at the given
+// capacity (if the threshold priority stays the same).
+//
+// Nodes in the pool always have either InactiveFlag or ActiveFlag set. A new node is
+// added to the pool by externally setting InactiveFlag. PriorityPool can switch a node
+// between InactiveFlag and ActiveFlag at any time. Nodes can be removed from the pool
+// by externally resetting both flags. ActiveFlag should not be set externally.
+//
+// The highest priority nodes in "inactive" state are moved to "active" state as soon as
+// the minimum capacity can be granted for them. The capacity of lower priority active
+// nodes is reduced or they are demoted to "inactive" state if their priority is
+// insufficient even at minimal capacity.
+type PriorityPool struct {
+	PriorityPoolSetup
+	ns                     *nodestate.NodeStateMachine
+	clock                  mclock.Clock
+	lock                   sync.Mutex
+	activeQueue            *prque.LazyQueue
+	inactiveQueue          *prque.Prque
+	changed                []*ppNodeInfo
+	activeCount, activeCap uint64
+	maxCount, maxCap       uint64
+	minCap                 uint64
+	activeBias             time.Duration
+	capacityStepDiv        uint64
+}
+
+// nodePriority interface provides current and estimated future priorities on demand
+type nodePriority interface {
+	// Priority should return the current priority of the node (higher is better)
+	Priority(now mclock.AbsTime, cap uint64) int64
+	// EstMinPriority should return a lower estimate for the minimum of the node priority
+	// value starting from the current moment until the given time. If the priority goes
+	// under the returned estimate before the specified moment then it is the caller's
+	// responsibility to signal with updateFlag.
+	EstMinPriority(until mclock.AbsTime, cap uint64, update bool) int64
+}
+
+// ppNodeInfo is the internal node descriptor of PriorityPool
+type ppNodeInfo struct {
+	nodePriority               nodePriority
+	node                       *enode.Node
+	connected                  bool
+	capacity, origCap          uint64
+	bias                       time.Duration
+	forced, changed            bool
+	activeIndex, inactiveIndex int
+}
+
+// NewPriorityPool creates a new PriorityPool
+func NewPriorityPool(ns *nodestate.NodeStateMachine, setup PriorityPoolSetup, clock mclock.Clock, minCap uint64, activeBias time.Duration, capacityStepDiv uint64) *PriorityPool {
+	pp := &PriorityPool{
+		ns:                ns,
+		PriorityPoolSetup: setup,
+		clock:             clock,
+		activeQueue:       prque.NewLazyQueue(activeSetIndex, activePriority, activeMaxPriority, clock, lazyQueueRefresh),
+		inactiveQueue:     prque.New(inactiveSetIndex),
+		minCap:            minCap,
+		activeBias:        activeBias,
+		capacityStepDiv:   capacityStepDiv,
+	}
+
+	ns.SubscribeField(pp.priorityField, func(node *enode.Node, state nodestate.Flags, oldValue, newValue interface{}) {
+		if newValue != nil {
+			c := &ppNodeInfo{
+				node:          node,
+				nodePriority:  newValue.(nodePriority),
+				activeIndex:   -1,
+				inactiveIndex: -1,
+			}
+			ns.SetFieldSub(node, pp.ppNodeInfoField, c)
+		} else {
+			ns.SetStateSub(node, nodestate.Flags{}, pp.ActiveFlag.Or(pp.InactiveFlag), 0)
+			if n, _ := pp.ns.GetField(node, pp.ppNodeInfoField).(*ppNodeInfo); n != nil {
+				pp.disconnectedNode(n)
+			}
+			ns.SetFieldSub(node, pp.CapacityField, nil)
+			ns.SetFieldSub(node, pp.ppNodeInfoField, nil)
+		}
+	})
+	ns.SubscribeState(pp.ActiveFlag.Or(pp.InactiveFlag), func(node *enode.Node, oldState, newState nodestate.Flags) {
+		if c, _ := pp.ns.GetField(node, pp.ppNodeInfoField).(*ppNodeInfo); c != nil {
+			if oldState.IsEmpty() {
+				pp.connectedNode(c)
+			}
+			if newState.IsEmpty() {
+				pp.disconnectedNode(c)
+			}
+		}
+	})
+	ns.SubscribeState(pp.updateFlag, func(node *enode.Node, oldState, newState nodestate.Flags) {
+		if !newState.IsEmpty() {
+			pp.updatePriority(node)
+		}
+	})
+	return pp
+}
+
+// RequestCapacity checks whether changing the capacity of a node to the given target
+// is possible (bias is applied in favor of other active nodes if the target is higher
+// than the current capacity).
+// If setCap is true then it also performs the change if possible. The function returns
+// the minimum priority needed to do the change and whether it is currently allowed.
+// If setCap and allowed are both true then the caller can assume that the change was
+// successful.
+// Note: priorityField should always be set before calling RequestCapacity. If setCap
+// is false then both InactiveFlag and ActiveFlag can be unset and they are not changed
+// by this function call either.
+// Note 2: this function should run inside a NodeStateMachine operation
+func (pp *PriorityPool) RequestCapacity(node *enode.Node, targetCap uint64, bias time.Duration, setCap bool) (minPriority int64, allowed bool) {
+	pp.lock.Lock()
+	pp.activeQueue.Refresh()
+	var updates []capUpdate
+	defer func() {
+		pp.lock.Unlock()
+		pp.updateFlags(updates)
+	}()
+
+	if targetCap < pp.minCap {
+		targetCap = pp.minCap
+	}
+	c, _ := pp.ns.GetField(node, pp.ppNodeInfoField).(*ppNodeInfo)
+	if c == nil {
+		log.Error("RequestCapacity called for unknown node", "id", node.ID())
+		return math.MaxInt64, false
+	}
+	var priority int64
+	if targetCap > c.capacity {
+		priority = c.nodePriority.EstMinPriority(pp.clock.Now()+mclock.AbsTime(bias), targetCap, false)
+	} else {
+		priority = c.nodePriority.Priority(pp.clock.Now(), targetCap)
+	}
+	pp.markForChange(c)
+	pp.setCapacity(c, targetCap)
+	c.forced = true
+	pp.activeQueue.Remove(c.activeIndex)
+	pp.inactiveQueue.Remove(c.inactiveIndex)
+	pp.activeQueue.Push(c)
+	minPriority = pp.enforceLimits()
+	// if capacity update is possible now then minPriority == math.MinInt64
+	// if it is not possible at all then minPriority == math.MaxInt64
+	allowed = priority > minPriority
+	updates = pp.finalizeChanges(setCap && allowed)
+	return
+}
+
+// SetLimits sets the maximum number and total capacity of simultaneously active nodes
+func (pp *PriorityPool) SetLimits(maxCount, maxCap uint64) {
+	pp.lock.Lock()
+	pp.activeQueue.Refresh()
+	var updates []capUpdate
+	defer func() {
+		pp.lock.Unlock()
+		pp.ns.Operation(func() { pp.updateFlags(updates) })
+	}()
+
+	inc := (maxCount > pp.maxCount) || (maxCap > pp.maxCap)
+	dec := (maxCount < pp.maxCount) || (maxCap < pp.maxCap)
+	pp.maxCount, pp.maxCap = maxCount, maxCap
+	if dec {
+		pp.enforceLimits()
+		updates = pp.finalizeChanges(true)
+	}
+	if inc {
+		updates = pp.tryActivate()
+	}
+}
+
+// SetActiveBias sets the bias applied when trying to activate inactive nodes
+func (pp *PriorityPool) SetActiveBias(bias time.Duration) {
+	pp.lock.Lock()
+	defer pp.lock.Unlock()
+
+	pp.activeBias = bias
+	pp.tryActivate()
+}
+
+// ActiveCapacity returns the total capacity of currently active nodes
+func (pp *PriorityPool) ActiveCapacity() uint64 {
+	pp.lock.Lock()
+	defer pp.lock.Unlock()
+
+	return pp.activeCap
+}
+
+// inactiveSetIndex callback updates ppNodeInfo item index in inactiveQueue
+func inactiveSetIndex(a interface{}, index int) {
+	a.(*ppNodeInfo).inactiveIndex = index
+}
+
+// activeSetIndex callback updates ppNodeInfo item index in activeQueue
+func activeSetIndex(a interface{}, index int) {
+	a.(*ppNodeInfo).activeIndex = index
+}
+
+// invertPriority inverts a priority value. The active queue uses inverted priorities
+// because the node on the top is the first to be deactivated.
+func invertPriority(p int64) int64 {
+	if p == math.MinInt64 {
+		return math.MaxInt64
+	}
+	return -p
+}
+
+// activePriority callback returns actual priority of ppNodeInfo item in activeQueue
+func activePriority(a interface{}, now mclock.AbsTime) int64 {
+	c := a.(*ppNodeInfo)
+	if c.forced {
+		return math.MinInt64
+	}
+	if c.bias == 0 {
+		return invertPriority(c.nodePriority.Priority(now, c.capacity))
+	} else {
+		return invertPriority(c.nodePriority.EstMinPriority(now+mclock.AbsTime(c.bias), c.capacity, true))
+	}
+}
+
+// activeMaxPriority callback returns estimated maximum priority of ppNodeInfo item in activeQueue
+func activeMaxPriority(a interface{}, until mclock.AbsTime) int64 {
+	c := a.(*ppNodeInfo)
+	if c.forced {
+		return math.MinInt64
+	}
+	return invertPriority(c.nodePriority.EstMinPriority(until+mclock.AbsTime(c.bias), c.capacity, false))
+}
+
+// inactivePriority callback returns actual priority of ppNodeInfo item in inactiveQueue
+func (pp *PriorityPool) inactivePriority(p *ppNodeInfo) int64 {
+	return p.nodePriority.Priority(pp.clock.Now(), pp.minCap)
+}
+
+// connectedNode is called when a new node has been added to the pool (InactiveFlag set)
+// Note: this function should run inside a NodeStateMachine operation
+func (pp *PriorityPool) connectedNode(c *ppNodeInfo) {
+	pp.lock.Lock()
+	pp.activeQueue.Refresh()
+	var updates []capUpdate
+	defer func() {
+		pp.lock.Unlock()
+		pp.updateFlags(updates)
+	}()
+
+	if c.connected {
+		return
+	}
+	c.connected = true
+	pp.inactiveQueue.Push(c, pp.inactivePriority(c))
+	updates = pp.tryActivate()
+}
+
+// disconnectedNode is called when a node has been removed from the pool (both InactiveFlag
+// and ActiveFlag reset)
+// Note: this function should run inside a NodeStateMachine operation
+func (pp *PriorityPool) disconnectedNode(c *ppNodeInfo) {
+	pp.lock.Lock()
+	pp.activeQueue.Refresh()
+	var updates []capUpdate
+	defer func() {
+		pp.lock.Unlock()
+		pp.updateFlags(updates)
+	}()
+
+	if !c.connected {
+		return
+	}
+	c.connected = false
+	pp.activeQueue.Remove(c.activeIndex)
+	pp.inactiveQueue.Remove(c.inactiveIndex)
+	if c.capacity != 0 {
+		pp.setCapacity(c, 0)
+		updates = pp.tryActivate()
+	}
+}
+
+// markForChange internally puts a node in a temporary state that can either be reverted
+// or confirmed later. This temporary state allows changing the capacity of a node and
+// moving it between the active and inactive queue. ActiveFlag/InactiveFlag and
+// CapacityField are not changed while the changes are still temporary.
+func (pp *PriorityPool) markForChange(c *ppNodeInfo) {
+	if c.changed {
+		return
+	}
+	c.changed = true
+	c.origCap = c.capacity
+	pp.changed = append(pp.changed, c)
+}
+
+// setCapacity changes the capacity of a node and adjusts activeCap and activeCount
+// accordingly. Note that this change is performed in the temporary state so it should
+// be called after markForChange and before finalizeChanges.
+func (pp *PriorityPool) setCapacity(n *ppNodeInfo, cap uint64) {
+	pp.activeCap += cap - n.capacity
+	if n.capacity == 0 {
+		pp.activeCount++
+	}
+	if cap == 0 {
+		pp.activeCount--
+	}
+	n.capacity = cap
+}
+
+// enforceLimits enforces active node count and total capacity limits. It returns the
+// lowest active node priority. Note that this function is performed on the temporary
+// internal state.
+func (pp *PriorityPool) enforceLimits() int64 {
+	if pp.activeCap <= pp.maxCap && pp.activeCount <= pp.maxCount {
+		return math.MinInt64
+	}
+	var maxActivePriority int64
+	pp.activeQueue.MultiPop(func(data interface{}, priority int64) bool {
+		c := data.(*ppNodeInfo)
+		pp.markForChange(c)
+		maxActivePriority = priority
+		if c.capacity == pp.minCap {
+			pp.setCapacity(c, 0)
+		} else {
+			sub := c.capacity / pp.capacityStepDiv
+			if c.capacity-sub < pp.minCap {
+				sub = c.capacity - pp.minCap
+			}
+			pp.setCapacity(c, c.capacity-sub)
+			pp.activeQueue.Push(c)
+		}
+		return pp.activeCap > pp.maxCap || pp.activeCount > pp.maxCount
+	})
+	return invertPriority(maxActivePriority)
+}
+
+// finalizeChanges either commits or reverts temporary changes. The necessary capacity
+// field and according flag updates are not performed here but returned in a list because
+// they should be performed while the mutex is not held.
+func (pp *PriorityPool) finalizeChanges(commit bool) (updates []capUpdate) {
+	for _, c := range pp.changed {
+		// always remove and push back in order to update biased/forced priority
+		pp.activeQueue.Remove(c.activeIndex)
+		pp.inactiveQueue.Remove(c.inactiveIndex)
+		c.bias = 0
+		c.forced = false
+		c.changed = false
+		if !commit {
+			pp.setCapacity(c, c.origCap)
+		}
+		if c.connected {
+			if c.capacity != 0 {
+				pp.activeQueue.Push(c)
+			} else {
+				pp.inactiveQueue.Push(c, pp.inactivePriority(c))
+			}
+			if c.capacity != c.origCap && commit {
+				updates = append(updates, capUpdate{c.node, c.origCap, c.capacity})
+			}
+		}
+		c.origCap = 0
+	}
+	pp.changed = nil
+	return
+}
+
+// capUpdate describes a CapacityField and ActiveFlag/InactiveFlag update
+type capUpdate struct {
+	node           *enode.Node
+	oldCap, newCap uint64
+}
+
+// updateFlags performs CapacityField and ActiveFlag/InactiveFlag updates while the
+// pool mutex is not held
+// Note: this function should run inside a NodeStateMachine operation
+func (pp *PriorityPool) updateFlags(updates []capUpdate) {
+	for _, f := range updates {
+		if f.oldCap == 0 {
+			pp.ns.SetStateSub(f.node, pp.ActiveFlag, pp.InactiveFlag, 0)
+		}
+		if f.newCap == 0 {
+			pp.ns.SetStateSub(f.node, pp.InactiveFlag, pp.ActiveFlag, 0)
+			pp.ns.SetFieldSub(f.node, pp.CapacityField, nil)
+		} else {
+			pp.ns.SetFieldSub(f.node, pp.CapacityField, f.newCap)
+		}
+	}
+}
+
+// tryActivate tries to activate inactive nodes if possible
+func (pp *PriorityPool) tryActivate() []capUpdate {
+	var commit bool
+	for pp.inactiveQueue.Size() > 0 {
+		c := pp.inactiveQueue.PopItem().(*ppNodeInfo)
+		pp.markForChange(c)
+		pp.setCapacity(c, pp.minCap)
+		c.bias = pp.activeBias
+		pp.activeQueue.Push(c)
+		pp.enforceLimits()
+		if c.capacity > 0 {
+			commit = true
+		} else {
+			break
+		}
+	}
+	return pp.finalizeChanges(commit)
+}
+
+// updatePriority gets the current priority value of the given node from the nodePriority
+// interface and performs the necessary changes. It is triggered by updateFlag.
+// Note: this function should run inside a NodeStateMachine operation
+func (pp *PriorityPool) updatePriority(node *enode.Node) {
+	pp.lock.Lock()
+	pp.activeQueue.Refresh()
+	var updates []capUpdate
+	defer func() {
+		pp.lock.Unlock()
+		pp.updateFlags(updates)
+	}()
+
+	c, _ := pp.ns.GetField(node, pp.ppNodeInfoField).(*ppNodeInfo)
+	if c == nil || !c.connected {
+		return
+	}
+	pp.activeQueue.Remove(c.activeIndex)
+	pp.inactiveQueue.Remove(c.inactiveIndex)
+	if c.capacity != 0 {
+		pp.activeQueue.Push(c)
+	} else {
+		pp.inactiveQueue.Push(c, pp.inactivePriority(c))
+	}
+	updates = pp.tryActivate()
+}
diff --git a/les/lespay/server/prioritypool_test.go b/les/lespay/server/prioritypool_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..cbb3f5b372acf13ae7872988eceb58aa92272073
--- /dev/null
+++ b/les/lespay/server/prioritypool_test.go
@@ -0,0 +1,129 @@
+// Copyright 2020 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
+
+package server
+
+import (
+	"math/rand"
+	"reflect"
+	"testing"
+
+	"github.com/ethereum/go-ethereum/common/mclock"
+	"github.com/ethereum/go-ethereum/p2p/enode"
+	"github.com/ethereum/go-ethereum/p2p/enr"
+	"github.com/ethereum/go-ethereum/p2p/nodestate"
+)
+
+var (
+	testSetup         = &nodestate.Setup{}
+	ppTestClientFlag  = testSetup.NewFlag("ppTestClientFlag")
+	ppTestClientField = testSetup.NewField("ppTestClient", reflect.TypeOf(&ppTestClient{}))
+	ppUpdateFlag      = testSetup.NewFlag("ppUpdateFlag")
+	ppTestSetup       = NewPriorityPoolSetup(testSetup)
+)
+
+func init() {
+	ppTestSetup.Connect(ppTestClientField, ppUpdateFlag)
+}
+
+const (
+	testCapacityStepDiv      = 100
+	testCapacityToleranceDiv = 10
+)
+
+type ppTestClient struct {
+	node         *enode.Node
+	balance, cap uint64
+}
+
+func (c *ppTestClient) Priority(now mclock.AbsTime, cap uint64) int64 {
+	return int64(c.balance / cap)
+}
+
+func (c *ppTestClient) EstMinPriority(until mclock.AbsTime, cap uint64, update bool) int64 {
+	return int64(c.balance / cap)
+}
+
+func TestPriorityPool(t *testing.T) {
+	clock := &mclock.Simulated{}
+	ns := nodestate.NewNodeStateMachine(nil, nil, clock, testSetup)
+
+	ns.SubscribeField(ppTestSetup.CapacityField, func(node *enode.Node, state nodestate.Flags, oldValue, newValue interface{}) {
+		if n := ns.GetField(node, ppTestSetup.priorityField); n != nil {
+			c := n.(*ppTestClient)
+			c.cap = newValue.(uint64)
+		}
+	})
+	pp := NewPriorityPool(ns, ppTestSetup, clock, 100, 0, testCapacityStepDiv)
+	ns.Start()
+	pp.SetLimits(100, 1000000)
+	clients := make([]*ppTestClient, 100)
+	raise := func(c *ppTestClient) {
+		for {
+			var ok bool
+			ns.Operation(func() {
+				_, ok = pp.RequestCapacity(c.node, c.cap+c.cap/testCapacityStepDiv, 0, true)
+			})
+			if !ok {
+				return
+			}
+		}
+	}
+	var sumBalance uint64
+	check := func(c *ppTestClient) {
+		expCap := 1000000 * c.balance / sumBalance
+		capTol := expCap / testCapacityToleranceDiv
+		if c.cap < expCap-capTol || c.cap > expCap+capTol {
+			t.Errorf("Wrong node capacity (expected %d, got %d)", expCap, c.cap)
+		}
+	}
+
+	for i := range clients {
+		c := &ppTestClient{
+			node:    enode.SignNull(&enr.Record{}, enode.ID{byte(i)}),
+			balance: 1000000000,
+			cap:     1000,
+		}
+		sumBalance += c.balance
+		clients[i] = c
+		ns.SetState(c.node, ppTestClientFlag, nodestate.Flags{}, 0)
+		ns.SetField(c.node, ppTestSetup.priorityField, c)
+		ns.SetState(c.node, ppTestSetup.InactiveFlag, nodestate.Flags{}, 0)
+		raise(c)
+		check(c)
+	}
+
+	for count := 0; count < 100; count++ {
+		c := clients[rand.Intn(len(clients))]
+		oldBalance := c.balance
+		c.balance = uint64(rand.Int63n(1000000000) + 1000000000)
+		sumBalance += c.balance - oldBalance
+		pp.ns.SetState(c.node, ppUpdateFlag, nodestate.Flags{}, 0)
+		pp.ns.SetState(c.node, nodestate.Flags{}, ppUpdateFlag, 0)
+		if c.balance > oldBalance {
+			raise(c)
+		} else {
+			for _, c := range clients {
+				raise(c)
+			}
+		}
+		for _, c := range clients {
+			check(c)
+		}
+	}
+
+	ns.Stop()
+}
diff --git a/les/metrics.go b/les/metrics.go
index c5edb61c3e64873798bb61964eee286a7446c613..9a79fd1bbd40402d11ee3336f5a61a3876cc8d61 100644
--- a/les/metrics.go
+++ b/les/metrics.go
@@ -99,8 +99,8 @@ var (
 	sqQueuedGauge        = metrics.NewRegisteredGauge("les/server/servingQueue/queued", nil)
 
 	clientConnectedMeter    = metrics.NewRegisteredMeter("les/server/clientEvent/connected", nil)
-	clientRejectedMeter     = metrics.NewRegisteredMeter("les/server/clientEvent/rejected", nil)
-	clientKickedMeter       = metrics.NewRegisteredMeter("les/server/clientEvent/kicked", nil)
+	clientActivatedMeter    = metrics.NewRegisteredMeter("les/server/clientEvent/activated", nil)
+	clientDeactivatedMeter  = metrics.NewRegisteredMeter("les/server/clientEvent/deactivated", nil)
 	clientDisconnectedMeter = metrics.NewRegisteredMeter("les/server/clientEvent/disconnected", nil)
 	clientFreezeMeter       = metrics.NewRegisteredMeter("les/server/clientEvent/freeze", nil)
 	clientErrorMeter        = metrics.NewRegisteredMeter("les/server/clientEvent/error", nil)
diff --git a/les/peer.go b/les/peer.go
index c529145239c7e2498723ffdc9854fe5c5d5448a9..0549daf9a6485355460678f7fc0448046bdcb7e1 100644
--- a/les/peer.go
+++ b/les/peer.go
@@ -33,6 +33,7 @@ import (
 	"github.com/ethereum/go-ethereum/eth"
 	"github.com/ethereum/go-ethereum/les/flowcontrol"
 	lpc "github.com/ethereum/go-ethereum/les/lespay/client"
+	lps "github.com/ethereum/go-ethereum/les/lespay/server"
 	"github.com/ethereum/go-ethereum/les/utils"
 	"github.com/ethereum/go-ethereum/light"
 	"github.com/ethereum/go-ethereum/p2p"
@@ -463,7 +464,7 @@ func (p *serverPeer) requestTxStatus(reqID uint64, txHashes []common.Hash) error
 	return p.sendRequest(GetTxStatusMsg, reqID, txHashes, len(txHashes))
 }
 
-// SendTxStatus creates a reply with a batch of transactions to be added to the remote transaction pool.
+// sendTxs creates a reply with a batch of transactions to be added to the remote transaction pool.
 func (p *serverPeer) sendTxs(reqID uint64, amount int, txs rlp.RawValue) error {
 	p.Log().Debug("Sending batch of transactions", "amount", amount, "size", len(txs))
 	sizeFactor := (len(txs) + txSizeCostLimit/2) / txSizeCostLimit
@@ -719,6 +720,8 @@ type clientPeer struct {
 	responseLock  sync.Mutex
 	responseCount uint64 // Counter to generate an unique id for request processing.
 
+	balance *lps.NodeBalance
+
 	// invalidLock is used for protecting invalidCount.
 	invalidLock  sync.RWMutex
 	invalidCount utils.LinearExpiredValue // Counter the invalid request the client peer has made.
@@ -876,18 +879,25 @@ func (p *clientPeer) sendAnnounce(request announceData) error {
 	return p2p.Send(p.rw, AnnounceMsg, request)
 }
 
+// allowInactive implements clientPoolPeer
+func (p *clientPeer) allowInactive() bool {
+	return false
+}
+
 // updateCapacity updates the request serving capacity assigned to a given client
 // and also sends an announcement about the updated flow control parameters
 func (p *clientPeer) updateCapacity(cap uint64) {
 	p.lock.Lock()
 	defer p.lock.Unlock()
 
-	p.fcParams = flowcontrol.ServerParams{MinRecharge: cap, BufLimit: cap * bufLimitRatio}
-	p.fcClient.UpdateParams(p.fcParams)
-	var kvList keyValueList
-	kvList = kvList.add("flowControl/MRR", cap)
-	kvList = kvList.add("flowControl/BL", cap*bufLimitRatio)
-	p.queueSend(func() { p.sendAnnounce(announceData{Update: kvList}) })
+	if cap != p.fcParams.MinRecharge {
+		p.fcParams = flowcontrol.ServerParams{MinRecharge: cap, BufLimit: cap * bufLimitRatio}
+		p.fcClient.UpdateParams(p.fcParams)
+		var kvList keyValueList
+		kvList = kvList.add("flowControl/MRR", cap)
+		kvList = kvList.add("flowControl/BL", cap*bufLimitRatio)
+		p.queueSend(func() { p.sendAnnounce(announceData{Update: kvList}) })
+	}
 }
 
 // freezeClient temporarily puts the client in a frozen state which means all
@@ -974,7 +984,7 @@ func (p *clientPeer) Handshake(td *big.Int, head common.Hash, headNum uint64, ge
 				// set default announceType on server side
 				p.announceType = announceTypeSimple
 			}
-			p.fcClient = flowcontrol.NewClientNode(server.fcManager, server.defParams)
+			p.fcClient = flowcontrol.NewClientNode(server.fcManager, p.fcParams)
 		}
 		return nil
 	})
diff --git a/les/server.go b/les/server.go
index ecb65150a8917e96657f19a24f7c5df736fc24cf..225a7ad1f03301e94b23dd068f2f170f5334d754 100644
--- a/les/server.go
+++ b/les/server.go
@@ -23,6 +23,7 @@ import (
 	"github.com/ethereum/go-ethereum/common/mclock"
 	"github.com/ethereum/go-ethereum/eth"
 	"github.com/ethereum/go-ethereum/les/flowcontrol"
+	lps "github.com/ethereum/go-ethereum/les/lespay/server"
 	"github.com/ethereum/go-ethereum/light"
 	"github.com/ethereum/go-ethereum/log"
 	"github.com/ethereum/go-ethereum/node"
@@ -51,9 +52,9 @@ type LesServer struct {
 	servingQueue *servingQueue
 	clientPool   *clientPool
 
-	minCapacity, maxCapacity, freeCapacity uint64
-	threadsIdle                            int // Request serving threads count when system is idle.
-	threadsBusy                            int // Request serving threads count when system is busy(block insertion).
+	minCapacity, maxCapacity uint64
+	threadsIdle              int // Request serving threads count when system is idle.
+	threadsBusy              int // Request serving threads count when system is busy(block insertion).
 
 	p2pSrv *p2p.Server
 }
@@ -94,7 +95,6 @@ func NewLesServer(node *node.Node, e *eth.Ethereum, config *eth.Config) (*LesSer
 	}
 	srv.handler = newServerHandler(srv, e.BlockChain(), e.ChainDb(), e.TxPool(), e.Synced)
 	srv.costTracker, srv.minCapacity = newCostTracker(e.ChainDb(), config)
-	srv.freeCapacity = srv.minCapacity
 	srv.oracle = srv.setupOracle(node, e.BlockChain().Genesis().Hash(), config)
 
 	// Initialize the bloom trie indexer.
@@ -102,8 +102,8 @@ func NewLesServer(node *node.Node, e *eth.Ethereum, config *eth.Config) (*LesSer
 
 	// Initialize server capacity management fields.
 	srv.defParams = flowcontrol.ServerParams{
-		BufLimit:    srv.freeCapacity * bufLimitRatio,
-		MinRecharge: srv.freeCapacity,
+		BufLimit:    srv.minCapacity * bufLimitRatio,
+		MinRecharge: srv.minCapacity,
 	}
 	// LES flow control tries to more or less guarantee the possibility for the
 	// clients to send a certain amount of requests at any time and get a quick
@@ -111,13 +111,13 @@ func NewLesServer(node *node.Node, e *eth.Ethereum, config *eth.Config) (*LesSer
 	// to send requests most of the time. Our goal is to serve as many clients as
 	// possible while the actually used server capacity does not exceed the limits
 	totalRecharge := srv.costTracker.totalRecharge()
-	srv.maxCapacity = srv.freeCapacity * uint64(srv.config.LightPeers)
+	srv.maxCapacity = srv.minCapacity * uint64(srv.config.LightPeers)
 	if totalRecharge > srv.maxCapacity {
 		srv.maxCapacity = totalRecharge
 	}
-	srv.fcManager.SetCapacityLimits(srv.freeCapacity, srv.maxCapacity, srv.freeCapacity*2)
-	srv.clientPool = newClientPool(srv.chainDb, srv.freeCapacity, mclock.System{}, func(id enode.ID) { go srv.peers.unregister(id.String()) })
-	srv.clientPool.setDefaultFactors(priceFactors{0, 1, 1}, priceFactors{0, 1, 1})
+	srv.fcManager.SetCapacityLimits(srv.minCapacity, srv.maxCapacity, srv.minCapacity*2)
+	srv.clientPool = newClientPool(srv.chainDb, srv.minCapacity, defaultConnectedBias, mclock.System{}, func(id enode.ID) { go srv.peers.unregister(id.String()) })
+	srv.clientPool.setDefaultFactors(lps.PriceFactors{TimeFactor: 0, CapacityFactor: 1, RequestFactor: 1}, lps.PriceFactors{TimeFactor: 0, CapacityFactor: 1, RequestFactor: 1})
 
 	checkpoint := srv.latestLocalCheckpoint()
 	if !checkpoint.Empty() {
@@ -268,7 +268,7 @@ func (s *LesServer) capacityManagement() {
 			updateRecharge()
 		case totalCapacity = <-totalCapacityCh:
 			totalCapacityGauge.Update(int64(totalCapacity))
-			newFreePeers := totalCapacity / s.freeCapacity
+			newFreePeers := totalCapacity / s.minCapacity
 			if newFreePeers < freePeers && newFreePeers < uint64(s.config.LightPeers) {
 				log.Warn("Reduced free peer connections", "from", freePeers, "to", newFreePeers)
 			}
diff --git a/les/server_handler.go b/les/server_handler.go
index 463f51cb43241edda3d4058c9d00ed8fcb955e67..583df960080c76e0192d4f2e5abe187687ce1eb9 100644
--- a/les/server_handler.go
+++ b/les/server_handler.go
@@ -31,6 +31,7 @@ import (
 	"github.com/ethereum/go-ethereum/core/state"
 	"github.com/ethereum/go-ethereum/core/types"
 	"github.com/ethereum/go-ethereum/ethdb"
+	lps "github.com/ethereum/go-ethereum/les/lespay/server"
 	"github.com/ethereum/go-ethereum/light"
 	"github.com/ethereum/go-ethereum/log"
 	"github.com/ethereum/go-ethereum/metrics"
@@ -138,10 +139,14 @@ func (h *serverHandler) handle(p *clientPeer) error {
 	defer p.fcClient.Disconnect()
 
 	// Disconnect the inbound peer if it's rejected by clientPool
-	if !h.server.clientPool.connect(p, 0) {
-		p.Log().Debug("Light Ethereum peer registration failed", "err", errFullClientPool)
+	if cap, err := h.server.clientPool.connect(p); cap != p.fcParams.MinRecharge || err != nil {
+		p.Log().Debug("Light Ethereum peer rejected", "err", errFullClientPool)
 		return errFullClientPool
 	}
+	p.balance, _ = h.server.clientPool.ns.GetField(p.Node(), h.server.clientPool.BalanceField).(*lps.NodeBalance)
+	if p.balance == nil {
+		return p2p.DiscRequested
+	}
 	// Register the peer locally
 	if err := h.server.peers.register(p); err != nil {
 		h.server.clientPool.disconnect(p)
@@ -157,6 +162,7 @@ func (h *serverHandler) handle(p *clientPeer) error {
 		wg.Wait() // Ensure all background task routines have exited.
 		h.server.peers.unregister(p.id)
 		h.server.clientPool.disconnect(p)
+		p.balance = nil
 		clientConnectionGauge.Update(int64(h.server.peers.len()))
 		connectionTimer.Update(time.Duration(mclock.Now() - connectedAt))
 	}()
@@ -256,13 +262,16 @@ func (h *serverHandler) handleMsg(p *clientPeer, wg *sync.WaitGroup) error {
 			realCost = maxCost // Assign a fake cost for testing purpose
 		} else {
 			realCost = h.server.costTracker.realCost(servingTime, msg.Size, replySize)
+			if realCost > maxCost {
+				realCost = maxCost
+			}
 		}
 		bv := p.fcClient.RequestProcessed(reqID, responseCount, maxCost, realCost)
 		if amount != 0 {
 			// Feed cost tracker request serving statistic.
 			h.server.costTracker.updateStats(msg.Code, amount, servingTime, realCost)
 			// Reduce priority "balance" for the specific peer.
-			h.server.clientPool.requestCost(p, realCost)
+			p.balance.RequestServed(realCost)
 		}
 		if reply != nil {
 			p.queueSend(func() {
@@ -380,7 +389,7 @@ func (h *serverHandler) handleMsg(p *clientPeer, wg *sync.WaitGroup) error {
 					first = false
 				}
 				reply := p.replyBlockHeaders(req.ReqID, headers)
-				sendResponse(req.ReqID, query.Amount, p.replyBlockHeaders(req.ReqID, headers), task.done())
+				sendResponse(req.ReqID, query.Amount, reply, task.done())
 				if metrics.EnabledExpensive {
 					miscOutHeaderPacketsMeter.Mark(1)
 					miscOutHeaderTrafficMeter.Mark(int64(reply.size()))
diff --git a/les/test_helper.go b/les/test_helper.go
index 4ce1d03c2bc056c1e5d53e2a35466c0d8757a33d..9f9b28721e446bb69a8f49e523b2d614f76d3882 100644
--- a/les/test_helper.go
+++ b/les/test_helper.go
@@ -282,9 +282,9 @@ func newTestServerHandler(blocks int, indexers []*core.ChainIndexer, db ethdb.Da
 		},
 		fcManager: flowcontrol.NewClientManager(nil, clock),
 	}
-	server.costTracker, server.freeCapacity = newCostTracker(db, server.config)
+	server.costTracker, server.minCapacity = newCostTracker(db, server.config)
 	server.costTracker.testCostList = testCostList(0) // Disable flow control mechanism.
-	server.clientPool = newClientPool(db, 1, clock, nil)
+	server.clientPool = newClientPool(db, testBufRecharge, defaultConnectedBias, clock, func(id enode.ID) {})
 	server.clientPool.setLimits(10000, 10000) // Assign enough capacity for clientpool
 	server.handler = newServerHandler(server, simulation.Blockchain(), db, txpool, func() bool { return true })
 	if server.oracle != nil {
diff --git a/les/utils/expiredvalue.go b/les/utils/expiredvalue.go
index 980156d21c9efd2c1c0f7398d542f6faba6ed800..55e82cee48192a09dbacdc1999dacd61f806ac95 100644
--- a/les/utils/expiredvalue.go
+++ b/les/utils/expiredvalue.go
@@ -18,6 +18,7 @@ package utils
 
 import (
 	"math"
+	"sync"
 
 	"github.com/ethereum/go-ethereum/common/mclock"
 )
@@ -124,6 +125,11 @@ func (e *ExpiredValue) SubExp(a ExpiredValue) {
 	}
 }
 
+// IsZero returns true if the value is zero
+func (e *ExpiredValue) IsZero() bool {
+	return e.Base == 0
+}
+
 // LinearExpiredValue is very similar with the expiredValue which the value
 // will continuously expired. But the different part is it's expired linearly.
 type LinearExpiredValue struct {
@@ -168,12 +174,20 @@ func (e *LinearExpiredValue) Add(amount int64, now mclock.AbsTime) uint64 {
 	return e.Val
 }
 
+// ValueExpirer controls value expiration rate
+type ValueExpirer interface {
+	SetRate(now mclock.AbsTime, rate float64)
+	SetLogOffset(now mclock.AbsTime, logOffset Fixed64)
+	LogOffset(now mclock.AbsTime) Fixed64
+}
+
 // Expirer changes logOffset with a linear rate which can be changed during operation.
 // It is not thread safe, if access by multiple goroutines is needed then it should be
 // encapsulated into a locked structure.
 // Note that if neither SetRate nor SetLogOffset are used during operation then LogOffset
 // is thread safe.
 type Expirer struct {
+	lock       sync.RWMutex
 	logOffset  Fixed64
 	rate       float64
 	lastUpdate mclock.AbsTime
@@ -182,6 +196,9 @@ type Expirer struct {
 // SetRate changes the expiration rate which is the inverse of the time constant in
 // nanoseconds.
 func (e *Expirer) SetRate(now mclock.AbsTime, rate float64) {
+	e.lock.Lock()
+	defer e.lock.Unlock()
+
 	dt := now - e.lastUpdate
 	if dt > 0 {
 		e.logOffset += Fixed64(logToFixedFactor * float64(dt) * e.rate)
@@ -192,12 +209,18 @@ func (e *Expirer) SetRate(now mclock.AbsTime, rate float64) {
 
 // SetLogOffset sets logOffset instantly.
 func (e *Expirer) SetLogOffset(now mclock.AbsTime, logOffset Fixed64) {
+	e.lock.Lock()
+	defer e.lock.Unlock()
+
 	e.lastUpdate = now
 	e.logOffset = logOffset
 }
 
 // LogOffset returns the current logarithmic offset.
 func (e *Expirer) LogOffset(now mclock.AbsTime) Fixed64 {
+	e.lock.RLock()
+	defer e.lock.RUnlock()
+
 	dt := now - e.lastUpdate
 	if dt <= 0 {
 		return e.logOffset
diff --git a/les/utils/timeutils.go b/les/utils/timeutils.go
new file mode 100644
index 0000000000000000000000000000000000000000..62a4285d1524b2886b5af5107460455a287676b9
--- /dev/null
+++ b/les/utils/timeutils.go
@@ -0,0 +1,69 @@
+// Copyright 2020 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
+
+package utils
+
+import (
+	"sync"
+	"time"
+
+	"github.com/ethereum/go-ethereum/common/mclock"
+)
+
+type UpdateTimer struct {
+	clock     mclock.Clock
+	lock      sync.Mutex
+	last      mclock.AbsTime
+	threshold time.Duration
+}
+
+func NewUpdateTimer(clock mclock.Clock, threshold time.Duration) *UpdateTimer {
+	// We don't accept the update threshold less than 0.
+	if threshold < 0 {
+		return nil
+	}
+	// Don't panic for lazy users
+	if clock == nil {
+		clock = mclock.System{}
+	}
+	return &UpdateTimer{
+		clock:     clock,
+		last:      clock.Now(),
+		threshold: threshold,
+	}
+}
+
+func (t *UpdateTimer) Update(callback func(diff time.Duration) bool) bool {
+	return t.UpdateAt(t.clock.Now(), callback)
+}
+
+func (t *UpdateTimer) UpdateAt(at mclock.AbsTime, callback func(diff time.Duration) bool) bool {
+	t.lock.Lock()
+	defer t.lock.Unlock()
+
+	diff := time.Duration(at - t.last)
+	if diff < 0 {
+		diff = 0
+	}
+	if diff < t.threshold {
+		return false
+	}
+	if callback(diff) {
+		t.last = at
+		return true
+	}
+	return false
+}
diff --git a/les/utils/timeutils_test.go b/les/utils/timeutils_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..9f9e1c2dc9389a0d494d2dffc9236f3df384d2cb
--- /dev/null
+++ b/les/utils/timeutils_test.go
@@ -0,0 +1,47 @@
+// Copyright 2020 The go-ethereum Authors
+// This file is part of the go-ethereum library.
+//
+// The go-ethereum library is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Lesser General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// The go-ethereum library is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Lesser General Public License for more details.
+//
+// You should have received a copy of the GNU Lesser General Public License
+// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
+
+package utils
+
+import (
+	"testing"
+	"time"
+
+	"github.com/ethereum/go-ethereum/common/mclock"
+)
+
+func TestUpdateTimer(t *testing.T) {
+	timer := NewUpdateTimer(mclock.System{}, -1)
+	if timer != nil {
+		t.Fatalf("Create update timer with negative threshold")
+	}
+	sim := &mclock.Simulated{}
+	timer = NewUpdateTimer(sim, time.Second)
+	if updated := timer.Update(func(diff time.Duration) bool { return true }); updated {
+		t.Fatalf("Update the clock without reaching the threshold")
+	}
+	sim.Run(time.Second)
+	if updated := timer.Update(func(diff time.Duration) bool { return true }); !updated {
+		t.Fatalf("Doesn't update the clock when reaching the threshold")
+	}
+	if updated := timer.UpdateAt(sim.Now()+mclock.AbsTime(time.Second), func(diff time.Duration) bool { return true }); !updated {
+		t.Fatalf("Doesn't update the clock when reaching the threshold")
+	}
+	timer = NewUpdateTimer(sim, 0)
+	if updated := timer.Update(func(diff time.Duration) bool { return true }); !updated {
+		t.Fatalf("Doesn't update the clock without threshold limitaion")
+	}
+}