diff --git a/Cargo.lock b/Cargo.lock
index e16232044a4e7b2d5f30ad592fa2021f010dade8..16591d4c5ae5a6e6866a3798cd1345982b6130fd 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1048,6 +1048,7 @@ dependencies = [
  "pallet-preimage",
  "pallet-provide-randomness",
  "pallet-proxy",
+ "pallet-quota",
  "pallet-scheduler",
  "pallet-session",
  "pallet-session-benchmarking",
@@ -3004,6 +3005,7 @@ dependencies = [
  "pallet-preimage",
  "pallet-provide-randomness",
  "pallet-proxy",
+ "pallet-quota",
  "pallet-scheduler",
  "pallet-session",
  "pallet-sudo",
@@ -3072,6 +3074,7 @@ dependencies = [
  "pallet-preimage",
  "pallet-provide-randomness",
  "pallet-proxy",
+ "pallet-quota",
  "pallet-scheduler",
  "pallet-session",
  "pallet-session-benchmarking",
@@ -3375,6 +3378,7 @@ dependencies = [
  "pallet-preimage",
  "pallet-provide-randomness",
  "pallet-proxy",
+ "pallet-quota",
  "pallet-scheduler",
  "pallet-session",
  "pallet-session-benchmarking",
@@ -3396,6 +3400,7 @@ dependencies = [
  "sp-block-builder",
  "sp-consensus-babe",
  "sp-core",
+ "sp-distance",
  "sp-inherents",
  "sp-io",
  "sp-keyring",
@@ -5791,7 +5796,10 @@ dependencies = [
  "log",
  "maplit",
  "pallet-balances",
+ "pallet-identity",
  "pallet-provide-randomness",
+ "pallet-quota",
+ "pallet-transaction-payment",
  "pallet-treasury",
  "parity-scale-codec",
  "scale-info",
@@ -6028,6 +6036,23 @@ dependencies = [
  "sp-std 5.0.0",
 ]
 
+[[package]]
+name = "pallet-quota"
+version = "3.0.0"
+dependencies = [
+ "frame-benchmarking",
+ "frame-support",
+ "frame-system",
+ "pallet-balances",
+ "pallet-identity",
+ "parity-scale-codec",
+ "scale-info",
+ "sp-core",
+ "sp-io",
+ "sp-runtime",
+ "sp-std 5.0.0",
+]
+
 [[package]]
 name = "pallet-scheduler"
 version = "4.0.0-dev"
diff --git a/Cargo.toml b/Cargo.toml
index a8064f606b01ac57a3aed100112a1f61e5a95b90..139256e47c2369424903a1c404046e2157a9cbbb 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -145,6 +145,7 @@ members = [
     'end2end-tests',
     'live-tests',
     'pallets/certification',
+    'pallets/quota',
     'pallets/distance',
     'pallets/duniter-test-parameters',
     'pallets/duniter-test-parameters/macro',
diff --git a/docs/api/runtime-calls.md b/docs/api/runtime-calls.md
index 95ffd53436b05d24801de03d0548b0b4c565bb06..f6a2ddb8aa0aad99e8e3e4100924948699223373 100644
--- a/docs/api/runtime-calls.md
+++ b/docs/api/runtime-calls.md
@@ -13,7 +13,20 @@ through on-chain governance mechanisms.
 
 ## User calls
 
-There are **69** user calls from **21** pallets.
+There are **80** user calls from **23** pallets.
+
+### Account - 1
+
+#### unlink_identity - 0
+
+<details><summary><code>unlink_identity()</code></summary>
+
+```rust
+```
+</details>
+
+
+unlink the identity associated with the account
 
 ### Scheduler - 2
 
@@ -88,7 +101,6 @@ call: Box<<T as Config>::RuntimeCall>
 
 Anonymously schedule a task after a delay.
 
-
 #### schedule_named_after - 5
 
 <details><summary><code>schedule_named_after(id, after, maybe_periodic, priority, call)</code></summary>
@@ -105,7 +117,6 @@ call: Box<<T as Config>::RuntimeCall>
 
 Schedule a named task after a delay.
 
-
 ### Babe - 3
 
 #### report_equivocation - 0
@@ -126,9 +137,9 @@ be reported.
 
 ### Balances - 6
 
-#### transfer - 0
+#### transfer_allow_death - 0
 
-<details><summary><code>transfer(dest, value)</code></summary>
+<details><summary><code>transfer_allow_death(dest, value)</code></summary>
 
 ```rust
 dest: AccountIdLookupOf<T>
@@ -139,12 +150,30 @@ value: T::Balance
 
 Transfer some liquid free balance to another account.
 
-`transfer` will set the `FreeBalance` of the sender and receiver.
+`transfer_allow_death` will set the `FreeBalance` of the sender and receiver.
 If the sender's account is below the existential deposit as a result
 of the transfer, the account will be reaped.
 
 The dispatch origin for this call must be `Signed` by the transactor.
 
+#### set_balance_deprecated - 1
+
+<details><summary><code>set_balance_deprecated(who, new_free, old_reserved)</code></summary>
+
+```rust
+who: AccountIdLookupOf<T>
+new_free: T::Balance
+old_reserved: T::Balance
+```
+</details>
+
+
+Set the regular balance of a given account; it also takes a reserved balance but this
+must be the same as the account's current reserved balance.
+
+The dispatch origin for this call is `root`.
+
+WARNING: This call is DEPRECATED! Use `force_set_balance` instead.
 
 #### transfer_keep_alive - 3
 
@@ -157,12 +186,12 @@ value: T::Balance
 </details>
 
 
-Same as the [`transfer`] call, but with a check that the transfer will not kill the
-origin account.
+Same as the [`transfer_allow_death`] call, but with a check that the transfer will not
+kill the origin account.
 
-99% of the time you want [`transfer`] instead.
+99% of the time you want [`transfer_allow_death`] instead.
 
-[`transfer`]: struct.Pallet.html#method.transfer
+[`transfer_allow_death`]: struct.Pallet.html#method.transfer
 
 #### transfer_all - 4
 
@@ -189,9 +218,56 @@ The dispatch origin of this call must be Signed.
 - `keep_alive`: A boolean to determine if the `transfer_all` operation should send all
   of the funds the account has, causing the sender account to be killed (false), or
   transfer everything except at least the existential deposit, which will guarantee to
-  keep the sender account alive (true). # <weight>
-- O(1). Just like transfer, but reading the user's transferable balance first.
-  #</weight>
+  keep the sender account alive (true).
+
+#### upgrade_accounts - 6
+
+<details><summary><code>upgrade_accounts(who)</code></summary>
+
+```rust
+who: Vec<T::AccountId>
+```
+</details>
+
+
+Upgrade a specified account.
+
+- `origin`: Must be `Signed`.
+- `who`: The account to be upgraded.
+
+This will waive the transaction fee if at least all but 10% of the accounts needed to
+be upgraded. (We let some not have to be upgraded just in order to allow for the
+possibililty of churn).
+
+#### transfer - 7
+
+<details><summary><code>transfer(dest, value)</code></summary>
+
+```rust
+dest: AccountIdLookupOf<T>
+value: T::Balance
+```
+</details>
+
+
+Alias for `transfer_allow_death`, provided only for name-wise compatibility.
+
+WARNING: DEPRECATED! Will be released in approximately 3 months.
+
+#### force_set_balance - 8
+
+<details><summary><code>force_set_balance(who, new_free)</code></summary>
+
+```rust
+who: AccountIdLookupOf<T>
+new_free: T::Balance
+```
+</details>
+
+
+Set the regular balance of a given account.
+
+The dispatch origin for this call is `root`.
 
 ### OneshotAccount - 7
 
@@ -290,6 +366,18 @@ keys: T::KeysWrapper
 
 declare new session keys to replace current ones
 
+#### remove_member_from_blacklist - 4
+
+<details><summary><code>remove_member_from_blacklist(member_id)</code></summary>
+
+```rust
+member_id: T::MemberId
+```
+</details>
+
+
+remove an identity from the blacklist
+
 ### Grandpa - 15
 
 #### report_equivocation - 0
@@ -407,6 +495,11 @@ Dispatch a proposal from a member using the `Member` origin.
 
 Origin must be a member of the collective.
 
+**Complexity**:
+- `O(B + M + P)` where:
+- `B` is `proposal` size in bytes (length-fee-bounded)
+- `M` members-count (code-bounded)
+- `P` complexity of dispatching `proposal`
 
 #### propose - 2
 
@@ -427,6 +520,13 @@ Requires the sender to be member.
 `threshold` determines whether `proposal` is executed directly (`threshold < 2`)
 or put up for voting.
 
+**Complexity**
+- `O(B + M + P1)` or `O(B + M + P2)` where:
+  - `B` is `proposal` size in bytes (length-fee-bounded)
+  - `M` is members-count (code- and governance-bounded)
+  - branching is influenced by `threshold` where:
+    - `P1` is proposal execution complexity (`threshold < 2`)
+    - `P2` is proposals-count (code-bounded) (`threshold >= 2`)
 
 #### vote - 3
 
@@ -447,38 +547,8 @@ Requires the sender to be a member.
 Transaction fees will be waived if the member is voting on any particular proposal
 for the first time and the call is successful. Subsequent vote changes will charge a
 fee.
-
-#### close_old_weight - 4
-
-<details><summary><code>close_old_weight(proposal_hash, index, proposal_weight_bound, length_bound)</code></summary>
-
-```rust
-proposal_hash: T::Hash
-index: ProposalIndex
-proposal_weight_bound: OldWeight
-length_bound: u32
-```
-</details>
-
-
-Close a vote that is either approved, disapproved or whose voting period has ended.
-
-May be called by any signed account in order to finish voting and close the proposal.
-
-If called before the end of the voting period it will only close the vote if it is
-has enough votes to be approved or disapproved.
-
-If called after the end of the voting period abstentions are counted as rejections
-unless there is a prime member set and the prime member cast an approval.
-
-If the close operation completes successfully with disapproval, the transaction fee will
-be waived. Otherwise execution of the approved operation will be charged to the caller.
-
-+ `proposal_weight_bound`: The maximum amount of weight consumed by executing the closed
-proposal.
-+ `length_bound`: The upper bound for the length of the proposal in storage. Checked via
-`storage::read` so it is `size_of::<u32>() == 4` larger than the pure length.
-
+**Complexity**
+- `O(M)` where `M` is members-count (code- and governance-bounded)
 
 #### close - 6
 
@@ -511,6 +581,12 @@ proposal.
 + `length_bound`: The upper bound for the length of the proposal in storage. Checked via
 `storage::read` so it is `size_of::<u32>() == 4` larger than the pure length.
 
+**Complexity**
+- `O(B + M + P1 + P2)` where:
+  - `B` is `proposal` size in bytes (length-fee-bounded)
+  - `M` is members-count (code- and governance-bounded)
+  - `P1` is the complexity of `proposal` preimage.
+  - `P2` is proposal-count (code-bounded)
 
 ### UniversalDividend - 30
 
@@ -603,7 +679,7 @@ validate the owned identity (must meet the main wot requirements)
 
 ```rust
 new_key: T::AccountId
-new_key_sig: T::NewOwnerKeySignature
+new_key_sig: T::Signature
 ```
 </details>
 
@@ -611,7 +687,7 @@ new_key_sig: T::NewOwnerKeySignature
 Change identity owner key.
 
 - `new_key`: the new owner key.
-- `new_key_sig`: the signature of the encoded form of `NewOwnerKeyPayload`.
+- `new_key_sig`: the signature of the encoded form of `IdtyIndexAccountIdPayload`.
                  Must be signed by `new_key`.
 
 The origin should be the old identity owner key.
@@ -623,7 +699,7 @@ The origin should be the old identity owner key.
 ```rust
 idty_index: T::IdtyIndex
 revocation_key: T::AccountId
-revocation_sig: T::RevocationSignature
+revocation_sig: T::Signature
 ```
 </details>
 
@@ -650,6 +726,19 @@ inc: bool
 
 change sufficient ref count for given key
 
+#### link_account - 8
+
+<details><summary><code>link_account(account_id, payload_sig)</code></summary>
+
+```rust
+account_id: T::AccountId
+payload_sig: T::Signature
+```
+</details>
+
+
+Link an account to an identity
+
 ### Membership - 42
 
 #### claim_membership - 1
@@ -694,6 +783,63 @@ Add a new certification or renew an existing one
 
 The origin must be allow to certify.
 
+### Distance - 44
+
+#### request_distance_evaluation - 0
+
+<details><summary><code>request_distance_evaluation()</code></summary>
+
+```rust
+```
+</details>
+
+
+Request an identity to be evaluated
+
+#### update_evaluation - 1
+
+<details><summary><code>update_evaluation(computation_result)</code></summary>
+
+```rust
+computation_result: ComputationResult
+```
+</details>
+
+
+(Inherent) Push an evaluation result to the pool
+
+#### force_update_evaluation - 2
+
+<details><summary><code>force_update_evaluation(evaluator, computation_result)</code></summary>
+
+```rust
+evaluator: <T as frame_system::Config>::AccountId
+computation_result: ComputationResult
+```
+</details>
+
+
+Push an evaluation result to the pool
+
+#### force_set_distance_status - 3
+
+<details><summary><code>force_set_distance_status(identity, status)</code></summary>
+
+```rust
+identity: <T as pallet_identity::Config>::IdtyIndex
+status: Option<(<T as frame_system::Config>::AccountId, DistanceStatus)>
+```
+</details>
+
+
+Set the distance evaluation status of an identity
+
+Removes the status if `status` is `None`.
+
+* `status.0` is the account for whom the price will be unreserved or slashed
+  when the evaluation completes.
+* `status.1` is the status of the evaluation.
+
 ### SmithMembership - 52
 
 #### request_membership - 0
@@ -851,6 +997,8 @@ multi-signature, but do not participate in the approval process.
 
 Result is equivalent to the dispatched result.
 
+**Complexity**
+O(Z + C) where Z is the length of the call and C its execution weight.
 
 #### as_multi - 1
 
@@ -892,6 +1040,19 @@ Result is equivalent to the dispatched result if `threshold` is exactly `1`. Oth
 on success, result is `Ok` and the result from the interior call, if it was executed,
 may be found in the deposited `MultisigExecuted` event.
 
+**Complexity**
+- `O(S + Z + Call)`.
+- Up to one balance-reserve or unreserve operation.
+- One passthrough operation, one insert, both `O(S)` where `S` is the number of
+  signatories. `S` is capped by `MaxSignatories`, with weight being proportional.
+- One call encode & hash, both of complexity `O(Z)` where `Z` is tx-len.
+- One encode & hash, both of complexity `O(S)`.
+- Up to one binary search and insert (`O(logS + S)`).
+- I/O: 1 read `O(S)`, up to 1 mutate `O(S)`. Up to one remove.
+- One event.
+- The weight of the `call`.
+- Storage: inserts one item, value size bounded by `MaxSignatories`, with a deposit
+  taken for its lifetime of `DepositBase + threshold * DepositFactor`.
 
 #### approve_as_multi - 2
 
@@ -926,6 +1087,17 @@ transaction index) of the first approval transaction.
 
 NOTE: If this is the final approval, you will want to use `as_multi` instead.
 
+**Complexity**
+- `O(S)`.
+- Up to one balance-reserve or unreserve operation.
+- One passthrough operation, one insert, both `O(S)` where `S` is the number of
+  signatories. `S` is capped by `MaxSignatories`, with weight being proportional.
+- One encode & hash, both of complexity `O(S)`.
+- Up to one binary search and insert (`O(logS + S)`).
+- I/O: 1 read `O(S)`, up to 1 mutate `O(S)`. Up to one remove.
+- One event.
+- Storage: inserts one item, value size bounded by `MaxSignatories`, with a deposit
+  taken for its lifetime of `DepositBase + threshold * DepositFactor`.
 
 #### cancel_as_multi - 3
 
@@ -952,6 +1124,15 @@ dispatch. May not be empty.
 transaction for this dispatch.
 - `call_hash`: The hash of the call to be executed.
 
+**Complexity**
+- `O(S)`.
+- Up to one balance-reserve or unreserve operation.
+- One passthrough operation, one insert, both `O(S)` where `S` is the number of
+  signatories. `S` is capped by `MaxSignatories`, with weight being proportional.
+- One encode & hash, both of complexity `O(S)`.
+- One event.
+- I/O: 1 read `O(S)`, one remove.
+- Storage: removes one item.
 
 ### ProvideRandomness - 62
 
@@ -985,8 +1166,6 @@ call: Box<<T as Config>::RuntimeCall>
 Dispatch the given `call` from an account that the sender is authorised for through
 `add_proxy`.
 
-Removes any corresponding announcement(s).
-
 The dispatch origin for this call must be _Signed_.
 
 Parameters:
@@ -1224,14 +1403,22 @@ calls: Vec<<T as Config>::RuntimeCall>
 
 Send a batch of dispatch calls.
 
-May be called from any origin.
+May be called from any origin except `None`.
 
 - `calls`: The calls to be dispatched from the same origin. The number of call must not
   exceed the constant: `batched_calls_limit` (available in constant metadata).
 
-If origin is root then call are dispatch without checking origin filter. (This includes
-bypassing `frame_system::Config::BaseCallFilter`).
+If origin is root then the calls are dispatched without checking origin filter. (This
+includes bypassing `frame_system::Config::BaseCallFilter`).
+
+**Complexity**
+- O(C) where C is the number of calls to be batched.
 
+This will return `Ok` in all circumstances. To determine the success of the batch, an
+event is deposited. If a call failed and the batch was interrupted, then the
+`BatchInterrupted` event is deposited, along with the number of successful calls made
+and the error of the failed call. If all were successful, then the `BatchCompleted`
+event is deposited.
 
 #### as_derivative - 1
 
@@ -1271,14 +1458,16 @@ calls: Vec<<T as Config>::RuntimeCall>
 Send a batch of dispatch calls and atomically execute them.
 The whole transaction will rollback and fail if any of the calls failed.
 
-May be called from any origin.
+May be called from any origin except `None`.
 
 - `calls`: The calls to be dispatched from the same origin. The number of call must not
   exceed the constant: `batched_calls_limit` (available in constant metadata).
 
-If origin is root then call are dispatch without checking origin filter. (This includes
-bypassing `frame_system::Config::BaseCallFilter`).
+If origin is root then the calls are dispatched without checking origin filter. (This
+includes bypassing `frame_system::Config::BaseCallFilter`).
 
+**Complexity**
+- O(C) where C is the number of calls to be batched.
 
 #### force_batch - 4
 
@@ -1293,14 +1482,34 @@ calls: Vec<<T as Config>::RuntimeCall>
 Send a batch of dispatch calls.
 Unlike `batch`, it allows errors and won't interrupt.
 
-May be called from any origin.
+May be called from any origin except `None`.
 
 - `calls`: The calls to be dispatched from the same origin. The number of call must not
   exceed the constant: `batched_calls_limit` (available in constant metadata).
 
-If origin is root then call are dispatch without checking origin filter. (This includes
-bypassing `frame_system::Config::BaseCallFilter`).
+If origin is root then the calls are dispatch without checking origin filter. (This
+includes bypassing `frame_system::Config::BaseCallFilter`).
+
+**Complexity**
+- O(C) where C is the number of calls to be batched.
+
+#### with_weight - 5
+
+<details><summary><code>with_weight(call, weight)</code></summary>
 
+```rust
+call: Box<<T as Config>::RuntimeCall>
+weight: Weight
+```
+</details>
+
+
+Dispatch a function call with a specified weight.
+
+This function does not check the weight of the call, and instead allows the
+Root origin to specify the weight of the call.
+
+The dispatch origin for this call must be _Root_.
 
 ### Treasury - 65
 
@@ -1319,6 +1528,8 @@ Put forward a suggestion for spending. A deposit proportional to the value
 is reserved and slashed if the proposal is rejected. It is returned once the
 proposal is awarded.
 
+**Complexity**
+- O(1)
 
 #### spend - 3
 
@@ -1356,28 +1567,23 @@ The original deposit will no longer be returned.
 May only be called from `T::RejectOrigin`.
 - `proposal_id`: The index of a proposal
 
+**Complexity**
+- O(A) where `A` is the number of approvals
+
+Errors:
+- `ProposalNotApproved`: The `proposal_id` supplied was not found in the approval queue,
+i.e., the proposal has not been approved. This could also mean the proposal does not
+exist altogether, thus there is no way it would have been approved in the first place.
 
 
 
 ## Root calls
 
-There are **22** root calls from **10** pallets.
+There are **20** root calls from **10** pallets.
 
 ### System - 0
 
-#### fill_block - 0
-
-<details><summary><code>fill_block(ratio)</code></summary>
-
-```rust
-ratio: Perbill
-```
-</details>
-
-
-A dispatch that will fill the block weight up to the given ratio.
-
-#### set_heap_pages - 2
+#### set_heap_pages - 1
 
 <details><summary><code>set_heap_pages(pages)</code></summary>
 
@@ -1389,7 +1595,7 @@ pages: u64
 
 Set the number of pages in the WebAssembly environment's heap.
 
-#### set_code - 3
+#### set_code - 2
 
 <details><summary><code>set_code(code)</code></summary>
 
@@ -1401,8 +1607,10 @@ code: Vec<u8>
 
 Set the new runtime code.
 
+**Complexity**
+- `O(C + S)` where `C` length of `code` and `S` complexity of `can_set_code`
 
-#### set_code_without_checks - 4
+#### set_code_without_checks - 3
 
 <details><summary><code>set_code_without_checks(code)</code></summary>
 
@@ -1414,8 +1622,10 @@ code: Vec<u8>
 
 Set the new runtime code without doing any checks of the given `code`.
 
+**Complexity**
+- `O(C)` where `C` length of `code`
 
-#### set_storage - 5
+#### set_storage - 4
 
 <details><summary><code>set_storage(items)</code></summary>
 
@@ -1427,7 +1637,7 @@ items: Vec<KeyValue>
 
 Set some items of storage.
 
-#### kill_storage - 6
+#### kill_storage - 5
 
 <details><summary><code>kill_storage(keys)</code></summary>
 
@@ -1439,7 +1649,7 @@ keys: Vec<Key>
 
 Kill some items from storage.
 
-#### kill_prefix - 7
+#### kill_prefix - 6
 
 <details><summary><code>kill_prefix(prefix, subkeys)</code></summary>
 
@@ -1474,27 +1684,6 @@ not been enacted yet.
 
 ### Balances - 6
 
-#### set_balance - 1
-
-<details><summary><code>set_balance(who, new_free, new_reserved)</code></summary>
-
-```rust
-who: AccountIdLookupOf<T>
-new_free: T::Balance
-new_reserved: T::Balance
-```
-</details>
-
-
-Set the balances of a given account.
-
-This will alter `FreeBalance` and `ReservedBalance` in storage. it will
-also alter the total issuance of the system (`TotalIssuance`) appropriately.
-If the new free or reserved balance is below the existential deposit,
-it will reset the account nonce (`frame_system::AccountNonce`).
-
-The dispatch origin for this call is `root`.
-
 #### force_transfer - 2
 
 <details><summary><code>force_transfer(source, dest, value)</code></summary>
@@ -1507,8 +1696,8 @@ value: T::Balance
 </details>
 
 
-Exactly as `transfer`, except the origin must be root and the source account may be
-specified.
+Exactly as `transfer_allow_death`, except the origin must be root and the source account
+may be specified.
 
 #### force_unreserve - 5
 
@@ -1586,7 +1775,7 @@ Set the collective's membership.
 - `old_count`: The upper bound for the previous number of members in storage. Used for
   weight estimation.
 
-Requires root origin.
+The dispatch of this call must be `SetMembersOrigin`.
 
 NOTE: Does not enforce the expected `MaxMembers` limit on the amount of members, but
       the weight estimations rely on it to estimate dispatchable weight.
@@ -1598,6 +1787,11 @@ implementation of the trait [`ChangeMembers`].
 Any call to `set_members` must be careful that the member set doesn't get out of sync
 with other logic managing the member set.
 
+**Complexity**:
+- `O(MP + N)` where:
+  - `M` old-members-count (code- and governance-bounded)
+  - `N` new-members-count (code- and governance-bounded)
+  - `P` proposals-count (code-bounded)
 
 #### disapprove_proposal - 5
 
@@ -1617,16 +1811,19 @@ Must be called by the Root origin.
 Parameters:
 * `proposal_hash`: The hash of the proposal that should be disapproved.
 
+**Complexity**
+O(P) where P is the number of max proposals
 
 ### Identity - 41
 
 #### remove_identity - 5
 
-<details><summary><code>remove_identity(idty_index, idty_name)</code></summary>
+<details><summary><code>remove_identity(idty_index, idty_name, reason)</code></summary>
 
 ```rust
 idty_index: T::IdtyIndex
 idty_name: Option<IdtyName>
+reason: IdtyRemovalReason<T::IdtyRemovalOtherReason>
 ```
 </details>
 
@@ -1716,6 +1913,8 @@ Dispatches a function call with a provided origin.
 
 The dispatch origin for this call must be _Root_.
 
+**Complexity**
+- O(1).
 
 
 
@@ -1728,7 +1927,7 @@ There are **6** disabled calls from **3** pallets.
 
 ### System - 0
 
-#### remark - 1
+#### remark - 0
 
 <details><summary><code>remark(remark)</code></summary>
 
@@ -1740,8 +1939,10 @@ remark: Vec<u8>
 
 Make some on-chain remark.
 
+**Complexity**
+- `O(1)`
 
-#### remark_with_event - 8
+#### remark_with_event - 7
 
 <details><summary><code>remark_with_event(remark)</code></summary>
 
@@ -1772,6 +1973,9 @@ This doesn't take effect until the next session.
 
 The dispatch origin of this function must be signed.
 
+**Complexity**
+- `O(1)`. Actual cost depends on the number of length of `T::Keys::key_ids()` which is
+  fixed.
 
 #### purge_keys - 1
 
@@ -1791,6 +1995,9 @@ convertible to a validator ID using the chain's typical addressing system (this
 means being a controller account) or directly convertible into a validator ID (which
 usually means being a stash account).
 
+**Complexity**
+- `O(1)` in number of key types. Actual cost depends on the number of length of
+  `T::Keys::key_ids()` which is fixed.
 
 ### Membership - 42
 
diff --git a/end2end-tests/cucumber-features/identity_creation.feature b/end2end-tests/cucumber-features/identity_creation.feature
index 899ef935d905c6ba71878953ed00610e16704187..ce660e1aead7bba6a689a0bbdf6ace610ce06831 100644
--- a/end2end-tests/cucumber-features/identity_creation.feature
+++ b/end2end-tests/cucumber-features/identity_creation.feature
@@ -3,11 +3,11 @@ Feature: Identity creation
   Scenario: alice invites a new member to join the web of trust
     # 6 ÄžD covers:
     # - account creation fees (3 ÄžD) 
-    # - existential deposit (2 ÄžD)
+    # - existential deposit (1 ÄžD)
     # - transaction fees (below 1 ÄžD)
     When alice sends 7 ÄžD to dave
-    # Alice is treasury funder for 1 ÄžD => 10-1-7 = 2
-    Then alice should have 2 ÄžD
+    # Alice is treasury funder for 1 ÄžD => 10-1-7 = 2 (minus fees)
+    Then alice should have 199 cÄžD
     When bob sends 750 cÄžD to dave
     When charlie sends 6 ÄžD to eve
     # alice last certification is counted from block zero
diff --git a/end2end-tests/cucumber-features/oneshot_account.feature b/end2end-tests/cucumber-features/oneshot_account.feature
index 462847fa0086085c4765c98c21465a8a7d557f3f..ebe5a19a14b7300548d55f211fc6d64bb3536614 100644
--- a/end2end-tests/cucumber-features/oneshot_account.feature
+++ b/end2end-tests/cucumber-features/oneshot_account.feature
@@ -2,22 +2,22 @@ Feature: Oneshot account
 
   Scenario: Simple oneshot consumption
     When alice sends 7 ÄžD to oneshot dave
-    # Alice is treasury funder for 1 ÄžD
-    Then alice should have 2 ÄžD
+    # Alice is treasury funder for 1 ÄžD and pays fees
+    Then alice should have 199 cÄžD
     Then dave should have oneshot 7 ÄžD
     When oneshot dave consumes into account bob
     Then dave should have oneshot 0 ÄžD
-    Then bob should have 1699 cÄžD
+    Then bob should have 1698 cÄžD
     Then bob should have oneshot 0 ÄžD
 
   Scenario: Double oneshot consumption
     When alice sends 7 ÄžD to oneshot dave
-    # Alice is treasury funder for 1 ÄžD
-    Then alice should have 2 ÄžD
+    # Alice is treasury funder for 1 ÄžD and pays fees
+    Then alice should have 199 cÄžD
     Then dave should have oneshot 7 ÄžD
     When oneshot dave consumes 4 ÄžD into account bob and the rest into oneshot charlie
     Then dave should have oneshot 0 ÄžD
     Then bob should have 14 ÄžD
     Then bob should have oneshot 0 ÄžD
     Then charlie should have 10 ÄžD
-    Then charlie should have oneshot 299 cÄžD
+    Then charlie should have oneshot 298 cÄžD
diff --git a/end2end-tests/cucumber-features/transfer_all.feature b/end2end-tests/cucumber-features/transfer_all.feature
index fafbc481d3507837f2219fe89580beba39180315..8b276df61d4cec58b488ebfdd7443c064ef686e3 100644
--- a/end2end-tests/cucumber-features/transfer_all.feature
+++ b/end2end-tests/cucumber-features/transfer_all.feature
@@ -6,9 +6,12 @@ Feature: Balance transfer all
     """
     Bob is a member, as such he is not allowed to empty his account completely,
     if he tries to do so, the existence deposit (1 ÄžD) must remain.
+    Bob is a member, transaction fees are refunded for him
+    101 = existential deposit (100) + fees refunded using quota (001)
     """
-    Then bob should have 1 ÄžD
+    Then bob should have 101 cÄžD
     """
     10 ÄžD (initial Bob balance) - 1 ÄžD (Existential deposit) - 0.02 ÄžD (transaction fees)
     """
     Then dave should have 898 cÄžD
+    # TODO check that the missing cent went to treasury
diff --git a/end2end-tests/tests/common/mod.rs b/end2end-tests/tests/common/mod.rs
index 41f3380cf013308b5e15259a789cc8e4d623d74b..b147b9783610074e6ced6f3f4e29fc28f7ee63d6 100644
--- a/end2end-tests/tests/common/mod.rs
+++ b/end2end-tests/tests/common/mod.rs
@@ -137,6 +137,8 @@ pub async fn spawn_node(
                 "--tmp",
                 // Fix: End2End test may fail due to network discovery. This option disables automatic peer discovery.Ï€
                 "--reserved-only",
+                // prevent local network discovery (even it does not connect due to above flag)
+                "--no-mdns",
             ],
             &duniter_binary_path,
             maybe_genesis_conf_file,
diff --git a/live-tests/tests/sanity_gdev.rs b/live-tests/tests/sanity_gdev.rs
index e0df612c599d09579998176ee9282369c5eb8046..766db91bd24a5005e087d9292ac69be2c26b6efb 100644
--- a/live-tests/tests/sanity_gdev.rs
+++ b/live-tests/tests/sanity_gdev.rs
@@ -38,7 +38,7 @@ type Index = u32;
 // Define gdev types
 type AccountInfo = gdev::runtime_types::frame_system::AccountInfo<
     Index,
-    gdev::runtime_types::pallet_duniter_account::types::AccountData<Balance>,
+    gdev::runtime_types::pallet_duniter_account::types::AccountData<Balance, IdtyIndex>,
 >;
 type IdtyData = gdev::runtime_types::common_runtime::entities::IdtyData;
 type IdtyIndex = u32;
diff --git a/node/src/chain_spec/gdev.rs b/node/src/chain_spec/gdev.rs
index f9d84c985926a2af46d4843a40c9e3167ef54c7d..0a23f5b5bdbe6c97e4799ada4436e07156b2fa0f 100644
--- a/node/src/chain_spec/gdev.rs
+++ b/node/src/chain_spec/gdev.rs
@@ -24,7 +24,7 @@ use common_runtime::*;
 use gdev_runtime::{
     opaque::SessionKeys, parameters, AccountConfig, AuthorityMembersConfig, BabeConfig,
     BalancesConfig, CertConfig, GenesisConfig, IdentityConfig, MembershipConfig, ParametersConfig,
-    SessionConfig, SmithCertConfig, SmithMembershipConfig, SudoConfig, SystemConfig,
+    QuotaConfig, SessionConfig, SmithCertConfig, SmithMembershipConfig, SudoConfig, SystemConfig,
     TechnicalCommitteeConfig, UniversalDividendConfig, WASM_BINARY,
 };
 use jsonrpsee::core::JsonValue;
@@ -302,6 +302,13 @@ fn genesis_data_to_gdev_genesis_conf(
             members: technical_committee_members,
             ..Default::default()
         },
+        quota: QuotaConfig {
+            identities: identities
+                .iter()
+                .enumerate()
+                .map(|(i, _)| i as u32 + 1)
+                .collect(),
+        },
         identity: IdentityConfig {
             identities: identities
                 .into_iter()
diff --git a/node/src/chain_spec/gen_genesis_data.rs b/node/src/chain_spec/gen_genesis_data.rs
index d5296ce217a09341323e36ed4787565799e4336e..4d6ef39ed4d85eb59ba3ba538e9510b52463baad 100644
--- a/node/src/chain_spec/gen_genesis_data.rs
+++ b/node/src/chain_spec/gen_genesis_data.rs
@@ -64,7 +64,7 @@ type MembershipData = sp_membership::MembershipData<u32>;
 
 #[derive(Clone)]
 pub struct GenesisData<Parameters: DeserializeOwned, SessionKeys: Decode> {
-    pub accounts: BTreeMap<AccountId, GenesisAccountData<u64>>,
+    pub accounts: BTreeMap<AccountId, GenesisAccountData<u64, u32>>,
     pub treasury_balance: u64,
     pub certs_by_receiver: BTreeMap<u32, BTreeMap<u32, Option<u32>>>,
     pub first_ud: Option<u64>,
@@ -237,7 +237,7 @@ struct SmithWoT<SK: Decode> {
 }
 
 struct GenesisInfo<'a> {
-    accounts: &'a BTreeMap<AccountId32, GenesisAccountData<u64>>,
+    accounts: &'a BTreeMap<AccountId32, GenesisAccountData<u64, u32>>,
     genesis_data_wallets_count: &'a usize,
     inactive_identities: &'a HashMap<u32, String>,
     identities: &'a Vec<GenesisIdentity>,
@@ -888,13 +888,13 @@ fn v1_wallets_to_v2_accounts(
 ) -> (
     bool,
     u64,
-    BTreeMap<AccountId32, GenesisAccountData<u64>>,
+    BTreeMap<AccountId32, GenesisAccountData<u64, u32>>,
     usize,
 ) {
     // monetary mass for double check
     let mut monetary_mass = 0u64;
     // account inserted in genesis
-    let mut accounts: BTreeMap<AccountId, GenesisAccountData<u64>> = BTreeMap::new();
+    let mut accounts: BTreeMap<AccountId, GenesisAccountData<u64, u32>> = BTreeMap::new();
     let mut invalid_wallets = 0;
     let mut fatal = false;
     for (pubkey, balance) in wallets {
@@ -917,7 +917,7 @@ fn v1_wallets_to_v2_accounts(
                 GenesisAccountData {
                     random_id: H256(blake2_256(&(balance, &owner_key).encode())),
                     balance,
-                    is_identity: false,
+                    idty_id: None,
                 },
             );
         } else {
@@ -1147,7 +1147,7 @@ fn make_authority_exist<SessionKeys: Encode, SKP: SessionKeysProvider<SessionKey
 }
 
 fn feed_identities(
-    accounts: &mut BTreeMap<AccountId32, GenesisAccountData<u64>>,
+    accounts: &mut BTreeMap<AccountId32, GenesisAccountData<u64, u32>>,
     identity_index: &mut HashMap<u32, String>,
     monetary_mass: &mut u64,
     inactive_identities: &mut HashMap<u32, String>,
@@ -1196,7 +1196,7 @@ fn feed_identities(
             GenesisAccountData {
                 random_id: H256(blake2_256(&(identity.index, &identity.owner_key).encode())),
                 balance: identity.balance,
-                is_identity: true,
+                idty_id: Some(identity.index),
             },
         );
 
@@ -1499,7 +1499,7 @@ where
                         &(i as u32 + idty_index_start, owner_key).encode(),
                     )),
                     balance: ud,
-                    is_identity: true,
+                    idty_id: Some(i as u32 + idty_index_start),
                 },
             )
         })
diff --git a/pallets/duniter-account/Cargo.toml b/pallets/duniter-account/Cargo.toml
index f55acb242d4a3f6652686e121146af6dc3ecd1b5..71bb9afe730747544b42d9fafc44d0fd12b37c7b 100644
--- a/pallets/duniter-account/Cargo.toml
+++ b/pallets/duniter-account/Cargo.toml
@@ -32,6 +32,8 @@ try-runtime = ['frame-support/try-runtime']
 
 [dependencies]
 # local
+pallet-quota = { path = "../quota", default-features = false }
+pallet-identity = { path = "../identity", default-features = false }
 pallet-provide-randomness = { path = "../provide-randomness", default-features = false }
 
 # crates.io
@@ -61,6 +63,11 @@ default-features = false
 git = 'https://github.com/duniter/substrate'
 branch = 'duniter-substrate-v0.9.42'
 
+[dependencies.pallet-transaction-payment]
+default-features = false
+git = 'https://github.com/duniter/substrate'
+branch = 'duniter-substrate-v0.9.42'
+
 [dependencies.pallet-treasury]
 default-features = false
 git = 'https://github.com/duniter/substrate'
diff --git a/pallets/duniter-account/README.md b/pallets/duniter-account/README.md
index d9b809d8af43bfdd3598696936445d0b38b5dc71..5d9e73b1432fb840b2c58869fe9cceb175cc0290 100644
--- a/pallets/duniter-account/README.md
+++ b/pallets/duniter-account/README.md
@@ -1,6 +1,6 @@
 # Duniter account pallet
 
-Duniter customizes the `AccountData` of the `Balances` Substrate pallet. In particular, it adds a field `RandomId`.
+Duniter customizes the `AccountData` of the `Balances` Substrate pallet. In particular, it adds the fields `random_id` and `linked_idty`. 
 
 ## RandomID
 
@@ -14,4 +14,8 @@ DuniterAccount defines a creation fee that is preleved to the account one block
 
 ## Sufficient 
 
-DuniterAccount tweaks the substrate AccountInfo to allow identity accounts to exist without existential deposit. This allows to spare the creation fee.
\ No newline at end of file
+DuniterAccount tweaks the substrate AccountInfo to allow identity accounts to exist without existential deposit. This allows to spare the creation fee.
+
+## Linked identity
+
+Duniter offers the possibility to link an account to an identity with the `linked_idty` field. It allows to request refund of transaction fees in `OnChargeTransaction`.
\ No newline at end of file
diff --git a/pallets/duniter-account/src/benchmarking.rs b/pallets/duniter-account/src/benchmarking.rs
index f78891665a283a292d92428c3eb4e06711821307..e0e300d0f6e57e5e09146cf76cbab56fe092d8cd 100644
--- a/pallets/duniter-account/src/benchmarking.rs
+++ b/pallets/duniter-account/src/benchmarking.rs
@@ -18,7 +18,7 @@
 
 use super::*;
 
-use frame_benchmarking::{benchmarks, whitelisted_caller};
+use frame_benchmarking::{account, benchmarks, whitelisted_caller};
 use frame_support::sp_runtime::{traits::One, Saturating};
 use frame_support::traits::{Currency, Get};
 use pallet_provide_randomness::OnFilledRandomness;
@@ -60,6 +60,10 @@ fn create_pending_accounts<T: Config>(
 }
 
 benchmarks! {
+    unlink_identity {
+        let account = account("Alice", 1, 1);
+        let origin = frame_system::RawOrigin::Signed(account);
+    }: _<T::RuntimeOrigin>(origin.into())
     on_initialize_sufficient  {
         let i in 0 .. T::MaxNewAccountsPerBlock::get() => create_pending_accounts::<T>(i, false, true)?;
     }: { Pallet::<T>::on_initialize(T::BlockNumber::one()); }
diff --git a/pallets/duniter-account/src/lib.rs b/pallets/duniter-account/src/lib.rs
index 908b768c8194751287dfa6eb6dd2b1efc244b08a..56095dcbee37700b3f6ae5bd431fd84f862d3e90 100644
--- a/pallets/duniter-account/src/lib.rs
+++ b/pallets/duniter-account/src/lib.rs
@@ -14,6 +14,8 @@
 // You should have received a copy of the GNU Affero General Public License
 // along with Duniter-v2S. If not, see <https://www.gnu.org/licenses/>.
 
+// Note: refund queue mechanism is inspired from frame contract
+
 #![cfg_attr(not(feature = "std"), no_std)]
 
 #[cfg(feature = "runtime-benchmarks")]
@@ -27,16 +29,24 @@ pub use types::*;
 pub use weights::WeightInfo;
 
 use frame_support::pallet_prelude::*;
+use frame_support::traits::{Currency, ExistenceRequirement, StorageVersion};
 use frame_support::traits::{OnUnbalanced, StoredMap};
 use frame_system::pallet_prelude::*;
+use pallet_identity::IdtyEvent;
 use pallet_provide_randomness::RequestId;
+use pallet_quota::traits::RefundFee;
+use pallet_transaction_payment::OnChargeTransaction;
 use sp_core::H256;
-use sp_runtime::traits::{Convert, Saturating};
+use sp_runtime::traits::{Convert, DispatchInfoOf, PostDispatchInfoOf, Saturating};
+use sp_std::fmt::Debug;
 
 #[frame_support::pallet]
 pub mod pallet {
     use super::*;
-    use frame_support::traits::{Currency, ExistenceRequirement, StorageVersion};
+    pub type IdtyIdOf<T> = <T as pallet_identity::Config>::IdtyIndex;
+    pub type CurrencyOf<T> = pallet_balances::Pallet<T>;
+    pub type BalanceOf<T> =
+        <CurrencyOf<T> as Currency<<T as frame_system::Config>::AccountId>>::Balance;
 
     /// The current storage version.
     const STORAGE_VERSION: StorageVersion = StorageVersion::new(1);
@@ -50,10 +60,12 @@ pub mod pallet {
 
     #[pallet::config]
     pub trait Config:
-        frame_system::Config<AccountData = AccountData<Self::Balance>>
+        frame_system::Config<AccountData = AccountData<Self::Balance, IdtyIdOf<Self>>>
         + pallet_balances::Config
         + pallet_provide_randomness::Config<Currency = pallet_balances::Pallet<Self>>
+        + pallet_transaction_payment::Config
         + pallet_treasury::Config<Currency = pallet_balances::Pallet<Self>>
+        + pallet_quota::Config
     {
         type AccountIdToSalt: Convert<Self::AccountId, [u8; 32]>;
         #[pallet::constant]
@@ -64,6 +76,10 @@ pub mod pallet {
         type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
         /// Type representing the weight of this pallet
         type WeightInfo: WeightInfo;
+        /// wrapped type
+        type InnerOnChargeTransaction: OnChargeTransaction<Self>;
+        /// type implementing refund behavior
+        type Refund: pallet_quota::traits::RefundFee<Self>;
     }
 
     // STORAGE //
@@ -82,8 +98,10 @@ pub mod pallet {
 
     #[pallet::genesis_config]
     pub struct GenesisConfig<T: Config> {
-        pub accounts:
-            sp_std::collections::btree_map::BTreeMap<T::AccountId, GenesisAccountData<T::Balance>>,
+        pub accounts: sp_std::collections::btree_map::BTreeMap<
+            T::AccountId,
+            GenesisAccountData<T::Balance, IdtyIdOf<T>>,
+        >,
         pub treasury_balance: T::Balance,
     }
 
@@ -107,7 +125,6 @@ pub mod pallet {
                     account.data.random_id = None;
                     account.data.free = self.treasury_balance;
                     account.providers = 1;
-                    account.sufficients = 1; // the treasury will always be self-sufficient
                 },
             );
 
@@ -129,16 +146,19 @@ pub mod pallet {
                 GenesisAccountData {
                     random_id,
                     balance,
-                    is_identity,
+                    idty_id,
                 },
             ) in &self.accounts
             {
                 // if the balance is below existential deposit, the account must be an identity
-                assert!(balance >= &T::ExistentialDeposit::get() || *is_identity);
+                assert!(balance >= &T::ExistentialDeposit::get() || idty_id.is_some());
                 // mutate account
                 frame_system::Account::<T>::mutate(account_id, |account| {
                     account.data.random_id = Some(*random_id);
                     account.data.free = *balance;
+                    if idty_id.is_some() {
+                        account.data.linked_idty = *idty_id;
+                    }
                     if balance >= &T::ExistentialDeposit::get() {
                         // accounts above existential deposit self-provide
                         account.providers = 1;
@@ -166,11 +186,64 @@ pub mod pallet {
         /// Random id assigned
         /// [account_id, random_id]
         RandomIdAssigned { who: T::AccountId, random_id: H256 },
+        /// account linked to identity
+        AccountLinked {
+            who: T::AccountId,
+            identity: IdtyIdOf<T>,
+        },
+        /// account unlinked from identity
+        AccountUnlinked(T::AccountId),
+    }
+
+    // CALLS //
+    #[pallet::call]
+    impl<T: Config> Pallet<T> {
+        /// unlink the identity associated with the account
+        #[pallet::call_index(0)]
+        #[pallet::weight(<T as pallet::Config>::WeightInfo::unlink_identity())]
+        pub fn unlink_identity(origin: OriginFor<T>) -> DispatchResultWithPostInfo {
+            let who = ensure_signed(origin)?;
+            Self::do_unlink_identity(who);
+            Ok(().into())
+        }
+    }
+
+    // INTERNAL FUNCTIONS //
+    impl<T: Config> Pallet<T> {
+        /// unlink account
+        pub fn do_unlink_identity(account_id: T::AccountId) {
+            // no-op if account already linked to nothing
+            frame_system::Account::<T>::mutate(&account_id, |account| {
+                if account.data.linked_idty.is_some() {
+                    Self::deposit_event(Event::AccountUnlinked(account_id.clone()));
+                }
+                account.data.linked_idty = None;
+            })
+        }
+
+        /// link account to identity
+        pub fn do_link_identity(account_id: T::AccountId, idty_id: IdtyIdOf<T>) {
+            // no-op if identity does not change
+            if frame_system::Account::<T>::get(&account_id)
+                .data
+                .linked_idty
+                != Some(idty_id)
+            {
+                frame_system::Account::<T>::mutate(&account_id, |account| {
+                    account.data.linked_idty = Some(idty_id);
+                    Self::deposit_event(Event::AccountLinked {
+                        who: account_id.clone(),
+                        identity: idty_id,
+                    });
+                })
+            }
+        }
     }
 
     // HOOKS //
     #[pallet::hooks]
     impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
+        // on initialize, withdraw account creation tax
         fn on_initialize(_: T::BlockNumber) -> Weight {
             let mut total_weight = Weight::zero();
             for account_id in PendingNewAccounts::<T>::iter_keys()
@@ -251,6 +324,17 @@ pub mod pallet {
     }
 }
 
+// implement account linker
+impl<T> pallet_identity::traits::LinkIdty<T::AccountId, IdtyIdOf<T>> for Pallet<T>
+where
+    T: Config,
+{
+    fn link_identity(account_id: T::AccountId, idty_id: IdtyIdOf<T>) {
+        Self::do_link_identity(account_id, idty_id);
+    }
+}
+
+// implement on filled randomness
 impl<T> pallet_provide_randomness::OnFilledRandomness for Pallet<T>
 where
     T: Config,
@@ -272,13 +356,14 @@ where
     }
 }
 
+// implement accountdata storedmap
 impl<T, AccountId, Balance>
     frame_support::traits::StoredMap<AccountId, pallet_balances::AccountData<Balance>> for Pallet<T>
 where
     AccountId: Parameter
         + Member
         + MaybeSerializeDeserialize
-        + core::fmt::Debug
+        + Debug
         + sp_runtime::traits::MaybeDisplay
         + Ord
         + Into<[u8; 32]>
@@ -290,11 +375,11 @@ where
         + Default
         + Copy
         + MaybeSerializeDeserialize
-        + core::fmt::Debug
+        + Debug
         + codec::MaxEncodedLen
         + scale_info::TypeInfo,
     T: Config
-        + frame_system::Config<AccountId = AccountId, AccountData = AccountData<Balance>>
+        + frame_system::Config<AccountId = AccountId, AccountData = AccountData<Balance, IdtyIdOf<T>>>
         + pallet_balances::Config<Balance = Balance>
         + pallet_provide_randomness::Config,
 {
@@ -307,7 +392,7 @@ where
         f: impl FnOnce(&mut Option<pallet_balances::AccountData<Balance>>) -> Result<R, E>,
     ) -> Result<R, E> {
         let account = frame_system::Account::<T>::get(account_id);
-        let was_providing = account.data != T::AccountData::default();
+        let was_providing = !account.data.free.is_zero() || !account.data.reserved.is_zero();
         let mut some_data = if was_providing {
             Some(account.data.into())
         } else {
@@ -346,3 +431,71 @@ where
         Ok(result)
     }
 }
+
+// ------
+// allows pay fees with quota instead of currency if available
+impl<T: Config> OnChargeTransaction<T> for Pallet<T>
+where
+    T::InnerOnChargeTransaction: OnChargeTransaction<
+        T,
+        Balance = <CurrencyOf<T> as Currency<T::AccountId>>::Balance,
+        LiquidityInfo = Option<<CurrencyOf<T> as Currency<T::AccountId>>::NegativeImbalance>,
+    >,
+{
+    type Balance = BalanceOf<T>;
+    type LiquidityInfo = Option<<CurrencyOf<T> as Currency<T::AccountId>>::NegativeImbalance>;
+
+    fn withdraw_fee(
+        who: &T::AccountId,
+        call: &T::RuntimeCall,
+        dispatch_info: &DispatchInfoOf<T::RuntimeCall>,
+        fee: Self::Balance,
+        tip: Self::Balance,
+    ) -> Result<Self::LiquidityInfo, TransactionValidityError> {
+        // does not change the withdraw fee step (still fallback to currency adapter or oneshot account)
+        T::InnerOnChargeTransaction::withdraw_fee(who, call, dispatch_info, fee, tip)
+    }
+
+    fn correct_and_deposit_fee(
+        who: &T::AccountId,
+        dispatch_info: &DispatchInfoOf<T::RuntimeCall>,
+        post_info: &PostDispatchInfoOf<T::RuntimeCall>,
+        corrected_fee: Self::Balance,
+        tip: Self::Balance,
+        already_withdrawn: Self::LiquidityInfo,
+    ) -> Result<(), TransactionValidityError> {
+        // in any case, the default behavior is applied
+        T::InnerOnChargeTransaction::correct_and_deposit_fee(
+            who,
+            dispatch_info,
+            post_info,
+            corrected_fee,
+            tip,
+            already_withdrawn,
+        )?;
+        // if account can be exonerated, add it to a refund queue
+        let account_data = frame_system::Pallet::<T>::get(who);
+        if let Some(idty_index) = account_data.linked_idty {
+            T::Refund::request_refund(who.clone(), idty_index, corrected_fee.saturating_sub(tip));
+        }
+        Ok(())
+    }
+}
+
+// implement identity event handler
+impl<T: Config> pallet_identity::traits::OnIdtyChange<T> for Pallet<T> {
+    fn on_idty_change(idty_id: IdtyIdOf<T>, idty_event: &IdtyEvent<T>) -> Weight {
+        match idty_event {
+            // link account to newly created identity
+            IdtyEvent::Created { owner_key, .. } => {
+                Self::do_link_identity(owner_key.clone(), idty_id);
+            }
+            IdtyEvent::Confirmed
+            | IdtyEvent::Validated
+            | IdtyEvent::ChangedOwnerKey { .. }
+            | IdtyEvent::Removed { .. } => {}
+        }
+        // TODO proper weight
+        Weight::zero()
+    }
+}
diff --git a/pallets/duniter-account/src/types.rs b/pallets/duniter-account/src/types.rs
index 09d9b795b8ea28988a690de60931d8bd624adc4e..c9b9a02cf4d3a83203086cb008847adde825122f 100644
--- a/pallets/duniter-account/src/types.rs
+++ b/pallets/duniter-account/src/types.rs
@@ -21,8 +21,8 @@ use sp_core::H256;
 use sp_runtime::traits::Zero;
 
 // see `struct AccountData` for details in substrate code
-#[derive(Clone, Decode, Default, Encode, Eq, MaxEncodedLen, PartialEq, RuntimeDebug, TypeInfo)]
-pub struct AccountData<Balance> {
+#[derive(Clone, Decode, Encode, Eq, MaxEncodedLen, PartialEq, RuntimeDebug, TypeInfo)] // Default,
+pub struct AccountData<Balance, IdtyId> {
     /// A random identifier that can not be chosen by the user
     // this intends to be used as a robust identification system
     pub(super) random_id: Option<H256>,
@@ -32,9 +32,26 @@ pub struct AccountData<Balance> {
     pub(super) reserved: Balance,
     // see Substrate AccountData
     fee_frozen: Balance,
+    /// an optional pointer to an identity
+    // used to know if this account is linked to a member
+    // used in quota system to refund fees
+    pub linked_idty: Option<IdtyId>,
+}
+
+// explicit implementation of default trait (can not be derived)
+impl<Balance: Zero, IdtyId> Default for AccountData<Balance, IdtyId> {
+    fn default() -> Self {
+        Self {
+            linked_idty: None,
+            random_id: None,
+            free: Balance::zero(),
+            reserved: Balance::zero(),
+            fee_frozen: Balance::zero(),
+        }
+    }
 }
 
-impl<Balance: Zero> AccountData<Balance> {
+impl<Balance: Zero, IdtyId> AccountData<Balance, IdtyId> {
     pub fn set_balances(&mut self, new_balances: pallet_balances::AccountData<Balance>) {
         self.free = new_balances.free;
         self.reserved = new_balances.reserved;
@@ -44,8 +61,10 @@ impl<Balance: Zero> AccountData<Balance> {
 
 // convert Duniter AccountData to Balances AccountData
 // needed for trait implementation
-impl<Balance: Zero> From<AccountData<Balance>> for pallet_balances::AccountData<Balance> {
-    fn from(account_data: AccountData<Balance>) -> Self {
+impl<Balance: Zero, IdtyId> From<AccountData<Balance, IdtyId>>
+    for pallet_balances::AccountData<Balance>
+{
+    fn from(account_data: AccountData<Balance, IdtyId>) -> Self {
         Self {
             free: account_data.free,
             reserved: account_data.reserved,
@@ -57,8 +76,8 @@ impl<Balance: Zero> From<AccountData<Balance>> for pallet_balances::AccountData<
 
 #[derive(Clone, Decode, Default, Encode, Eq, MaxEncodedLen, PartialEq, RuntimeDebug, TypeInfo)]
 #[cfg_attr(feature = "std", derive(serde::Deserialize, serde::Serialize))]
-pub struct GenesisAccountData<Balance> {
+pub struct GenesisAccountData<Balance, IdtyId> {
     pub random_id: H256,
     pub balance: Balance,
-    pub is_identity: bool,
+    pub idty_id: Option<IdtyId>,
 }
diff --git a/pallets/duniter-account/src/weights.rs b/pallets/duniter-account/src/weights.rs
index cfdd6844699beae7274e34f6dfddac2ff2dcd89b..212eb7fe4f6fe00e35abbc052926e6b1fa2a3ce7 100644
--- a/pallets/duniter-account/src/weights.rs
+++ b/pallets/duniter-account/src/weights.rs
@@ -25,10 +25,22 @@ pub trait WeightInfo {
     fn on_initialize_no_balance(i: u32) -> Weight;
     fn on_filled_randomness_pending() -> Weight;
     fn on_filled_randomness_no_pending() -> Weight;
+    fn unlink_identity() -> Weight;
 }
 
 // Insecure weights implementation, use it for tests only!
 impl WeightInfo for () {
+    /// Storage: System Account (r:1 w:0)
+    /// Proof: System Account (max_values: None, max_size: Some(126), added: 2601, mode: MaxEncodedLen)
+    fn unlink_identity() -> Weight {
+        // Proof Size summary in bytes:
+        //  Measured:  `0`
+        //  Estimated: `3591`
+        // Minimum execution time: 95_130_000 picoseconds.
+        Weight::from_parts(110_501_000, 0)
+            .saturating_add(Weight::from_parts(0, 3591))
+            .saturating_add(RocksDbWeight::get().reads(1))
+    }
     // Storage: Account PendingNewAccounts (r:1 w:0)
     // Storage: ProvideRandomness RequestIdProvider (r:1 w:1)
     // Storage: ProvideRandomness RequestsIds (r:1 w:1)
diff --git a/pallets/duniter-wot/src/lib.rs b/pallets/duniter-wot/src/lib.rs
index f4e336c1e4d594b11951cc5e87602ea8feac1426..1b3b9bdb6237a2efd4a48c0e3d2bc8ec451c50e0 100644
--- a/pallets/duniter-wot/src/lib.rs
+++ b/pallets/duniter-wot/src/lib.rs
@@ -107,8 +107,10 @@ pub mod pallet {
 
     #[pallet::error]
     pub enum Error<T, I = ()> {
-        /// Identity not allowed to claim membership
-        IdtyNotAllowedToClaimMembership,
+        /// Not enough certifications received to claim membership
+        NotEnoughCertsToClaimMembership,
+        /// Distance has not been evaluated positively
+        DistanceNotOK,
         /// Identity not allowed to request membership
         IdtyNotAllowedToRequestMembership,
         /// Identity not allowed to renew membership
@@ -268,9 +270,12 @@ impl<T: Config<I>, I: 'static> sp_membership::traits::CheckMembershipCallAllowed
     fn check_idty_allowed_to_claim_membership(idty_index: &IdtyIndex) -> Result<(), DispatchError> {
         let idty_cert_meta = pallet_certification::Pallet::<T, I>::idty_cert_meta(idty_index);
         ensure!(
-            idty_cert_meta.received_count >= T::MinCertForMembership::get()
-                && T::IsDistanceOk::is_distance_ok(idty_index),
-            Error::<T, I>::IdtyNotAllowedToClaimMembership
+            idty_cert_meta.received_count >= T::MinCertForMembership::get(),
+            Error::<T, I>::NotEnoughCertsToClaimMembership
+        );
+        ensure!(
+            T::IsDistanceOk::is_distance_ok(idty_index),
+            Error::<T, I>::DistanceNotOK,
         );
         Ok(())
     }
@@ -279,10 +284,13 @@ impl<T: Config<I>, I: 'static> sp_membership::traits::CheckMembershipCallAllowed
     fn check_idty_allowed_to_renew_membership(idty_index: &IdtyIndex) -> Result<(), DispatchError> {
         if let Some(idty_value) = pallet_identity::Pallet::<T>::identity(idty_index) {
             ensure!(
-                idty_value.status == IdtyStatus::Validated
-                    && T::IsDistanceOk::is_distance_ok(idty_index),
+                idty_value.status == IdtyStatus::Validated,
                 Error::<T, I>::IdtyNotAllowedToRenewMembership
             );
+            ensure!(
+                T::IsDistanceOk::is_distance_ok(idty_index),
+                Error::<T, I>::DistanceNotOK,
+            );
         } else {
             return Err(Error::<T, I>::IdtyNotFound.into());
         }
@@ -333,7 +341,7 @@ where
 impl<T: Config<I>, I: 'static> pallet_identity::traits::OnIdtyChange<T> for Pallet<T, I> {
     fn on_idty_change(idty_index: IdtyIndex, idty_event: &IdtyEvent<T>) -> Weight {
         match idty_event {
-            IdtyEvent::Created { creator } => {
+            IdtyEvent::Created { creator, .. } => {
                 if let Err(e) = <pallet_certification::Pallet<T, I>>::do_add_cert_checked(
                     *creator, idty_index, true,
                 ) {
diff --git a/pallets/duniter-wot/src/mock.rs b/pallets/duniter-wot/src/mock.rs
index 7bdf69631d1db82523331a818366aa870f3ffe8a..cd25f68a9e096f13024444a13d6b82271757321f 100644
--- a/pallets/duniter-wot/src/mock.rs
+++ b/pallets/duniter-wot/src/mock.rs
@@ -127,13 +127,12 @@ impl pallet_identity::Config for Test {
     type IdtyData = ();
     type IdtyNameValidator = IdtyNameValidatorTestImpl;
     type IdtyIndex = IdtyIndex;
+    type AccountLinker = ();
     type IdtyRemovalOtherReason = IdtyRemovalWotReason;
-    type NewOwnerKeySigner = UintAuthorityId;
-    type NewOwnerKeySignature = TestSignature;
+    type Signer = UintAuthorityId;
+    type Signature = TestSignature;
     type OnIdtyChange = DuniterWot;
     type RemoveIdentityConsumers = ();
-    type RevocationSigner = UintAuthorityId;
-    type RevocationSignature = TestSignature;
     type RuntimeEvent = RuntimeEvent;
     type WeightInfo = ();
     #[cfg(feature = "runtime-benchmarks")]
diff --git a/pallets/duniter-wot/src/tests.rs b/pallets/duniter-wot/src/tests.rs
index e45e390490ca5a3618ffbe2765bf57c36c4ddb9a..6cefede1f13db6e67eedb9ed7efec3f694af8f7b 100644
--- a/pallets/duniter-wot/src/tests.rs
+++ b/pallets/duniter-wot/src/tests.rs
@@ -21,8 +21,8 @@ use codec::Encode;
 use frame_support::instances::{Instance1, Instance2};
 use frame_support::{assert_noop, assert_ok};
 use pallet_identity::{
-    IdtyName, IdtyStatus, NewOwnerKeyPayload, RevocationPayload, NEW_OWNER_KEY_PAYLOAD_PREFIX,
-    REVOCATION_PAYLOAD_PREFIX,
+    IdtyIndexAccountIdPayload, IdtyName, IdtyStatus, RevocationPayload,
+    NEW_OWNER_KEY_PAYLOAD_PREFIX, REVOCATION_PAYLOAD_PREFIX,
 };
 use sp_runtime::testing::TestSignature;
 
@@ -123,7 +123,7 @@ fn test_smith_member_cant_change_its_idty_address() {
         run_to_block(2);
 
         let genesis_hash = System::block_hash(0);
-        let new_key_payload = NewOwnerKeyPayload {
+        let new_key_payload = IdtyIndexAccountIdPayload {
             genesis_hash: &genesis_hash,
             idty_index: 3u32,
             old_owner_key: &3u64,
@@ -495,7 +495,7 @@ fn test_certification_expire() {
         // Alice can not claim her membership because she does not have enough certifications
         assert_noop!(
             Membership::claim_membership(RuntimeOrigin::signed(1)),
-            pallet_duniter_wot::Error::<Test, Instance1>::IdtyNotAllowedToClaimMembership
+            pallet_duniter_wot::Error::<Test, Instance1>::NotEnoughCertsToClaimMembership
         );
 
         // --- BLOCK 23 ---
diff --git a/pallets/identity/src/benchmarking.rs b/pallets/identity/src/benchmarking.rs
index b1ddf6b685be8aec66d02b5a9b6e82c79807d9d3..c3df1b542ab9f1e567e652cb8f79039968e185c9 100644
--- a/pallets/identity/src/benchmarking.rs
+++ b/pallets/identity/src/benchmarking.rs
@@ -107,8 +107,8 @@ fn create_identities<T: Config>(i: u32) -> Result<(), &'static str> {
 benchmarks! {
     where_clause {
         where
-            T::NewOwnerKeySignature: From<sp_core::sr25519::Signature>,
-            T::RevocationSignature: From<sp_core::sr25519::Signature>,
+            T::Signature: From<sp_core::sr25519::Signature>,
+            T::Signature: From<sp_core::sr25519::Signature>,
             T::AccountId: From<AccountId32>,
             T::IdtyIndex: From<u32>,
     }
@@ -161,7 +161,7 @@ benchmarks! {
 
         // Change key a first time to add an old-old key
         let genesis_hash = frame_system::Pallet::<T>::block_hash(T::BlockNumber::zero());
-        let new_key_payload = NewOwnerKeyPayload {
+        let new_key_payload = IdtyIndexAccountIdPayload {
             genesis_hash: &genesis_hash,
             idty_index: account.index,
             old_owner_key: &account.key,
@@ -176,7 +176,7 @@ benchmarks! {
         //  The sufficients for the old_old key will drop to 0 during benchmark
         let caller_origin: <T as frame_system::Config>::RuntimeOrigin = RawOrigin::Signed(caller.clone()).into();
         let genesis_hash = frame_system::Pallet::<T>::block_hash(T::BlockNumber::zero());
-        let new_key_payload = NewOwnerKeyPayload {
+        let new_key_payload = IdtyIndexAccountIdPayload {
             genesis_hash: &genesis_hash,
             idty_index: account.index,
             old_owner_key: &caller_public,
@@ -199,7 +199,7 @@ benchmarks! {
         // Change key
         //  The sufficients for the old key will drop to 0 during benchmark
         let genesis_hash = frame_system::Pallet::<T>::block_hash(T::BlockNumber::zero());
-        let new_key_payload = NewOwnerKeyPayload {
+        let new_key_payload = IdtyIndexAccountIdPayload {
             genesis_hash: &genesis_hash,
             idty_index: account.index,
             old_owner_key: &account.key,
@@ -265,6 +265,17 @@ benchmarks! {
         assert!(sufficient < frame_system::Pallet::<T>::sufficients(&account.key), "Sufficient not incremented");
     }
 
+    link_account {
+        let alice_origin = RawOrigin::Signed(Identities::<T>::get(T::IdtyIndex::one()).unwrap().owner_key);
+        let bob_public = sr25519_generate(0.into(), None);
+        let bob: T::AccountId = MultiSigner::Sr25519(bob_public).into_account().into();
+        let genesis_hash = frame_system::Pallet::<T>::block_hash(T::BlockNumber::zero());
+        let payload = (
+            LINK_IDTY_PAYLOAD_PREFIX, genesis_hash, T::IdtyIndex::one(), bob.clone(),
+        ).encode();
+        let signature = sr25519_sign(0.into(), &bob_public, &payload).unwrap().into();
+    }: _<T::RuntimeOrigin>(alice_origin.into(), bob, signature)
+
     impl_benchmark_test_suite!(
         Pallet,
         // Create genesis identity Alice to test benchmark in mock
diff --git a/pallets/identity/src/lib.rs b/pallets/identity/src/lib.rs
index ed40d67c4a4f69805d728343bb3ad4728299bf17..8c96cc6a9ed8400f1c1d04a818a929688ee6d616 100644
--- a/pallets/identity/src/lib.rs
+++ b/pallets/identity/src/lib.rs
@@ -41,8 +41,12 @@ use sp_runtime::traits::{AtLeast32BitUnsigned, IdentifyAccount, One, Saturating,
 use sp_std::fmt::Debug;
 use sp_std::prelude::*;
 
+// icok = identity change owner key
 pub const NEW_OWNER_KEY_PAYLOAD_PREFIX: [u8; 4] = [b'i', b'c', b'o', b'k'];
+// revo = revocation
 pub const REVOCATION_PAYLOAD_PREFIX: [u8; 4] = [b'r', b'e', b'v', b'o'];
+// link = link (identity with account)
+pub const LINK_IDTY_PAYLOAD_PREFIX: [u8; 4] = [b'l', b'i', b'n', b'k'];
 
 #[frame_support::pallet]
 pub mod pallet {
@@ -93,23 +97,21 @@ pub mod pallet {
             + MaybeSerializeDeserialize
             + Debug
             + MaxEncodedLen;
+        /// custom type for account data
+        type AccountLinker: LinkIdty<Self::AccountId, Self::IdtyIndex>;
         /// Handle logic to validate an identity name
         type IdtyNameValidator: IdtyNameValidator;
         /// Additional reasons for identity removal
         type IdtyRemovalOtherReason: Clone + Codec + Debug + Eq + TypeInfo;
         /// On identity confirmed by its owner
         type OnIdtyChange: OnIdtyChange<Self>;
-        /// Signing key of new owner key payload
-        type NewOwnerKeySigner: IdentifyAccount<AccountId = Self::AccountId>;
-        /// Signature of new owner key payload
-        type NewOwnerKeySignature: Parameter + Verify<Signer = Self::NewOwnerKeySigner>;
+        /// Signing key of a payload
+        type Signer: IdentifyAccount<AccountId = Self::AccountId>;
+        /// Signature of a payload
+        type Signature: Parameter + Verify<Signer = Self::Signer>;
         /// Handle the logic that removes all identity consumers.
         /// "identity consumers" meaning all things that rely on the existence of the identity.
         type RemoveIdentityConsumers: RemoveIdentityConsumers<Self::IdtyIndex>;
-        /// Signing key of revocation payload
-        type RevocationSigner: IdentifyAccount<AccountId = Self::AccountId>;
-        /// Signature of revocation payload
-        type RevocationSignature: Parameter + Verify<Signer = Self::RevocationSigner>;
         /// Because this pallet emits events, it depends on the runtime's definition of an event.
         type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
         /// Type representing the weight of this pallet
@@ -331,9 +333,9 @@ pub mod pallet {
             IdentityIndexOf::<T>::insert(owner_key.clone(), idty_index);
             Self::deposit_event(Event::IdtyCreated {
                 idty_index,
-                owner_key,
+                owner_key: owner_key.clone(),
             });
-            T::OnIdtyChange::on_idty_change(idty_index, &IdtyEvent::Created { creator });
+            T::OnIdtyChange::on_idty_change(idty_index, &IdtyEvent::Created { creator, owner_key });
             Ok(().into())
         }
 
@@ -418,7 +420,7 @@ pub mod pallet {
         /// Change identity owner key.
         ///
         /// - `new_key`: the new owner key.
-        /// - `new_key_sig`: the signature of the encoded form of `NewOwnerKeyPayload`.
+        /// - `new_key_sig`: the signature of the encoded form of `IdtyIndexAccountIdPayload`.
         ///                  Must be signed by `new_key`.
         ///
         /// The origin should be the old identity owner key.
@@ -427,7 +429,7 @@ pub mod pallet {
         pub fn change_owner_key(
             origin: OriginFor<T>,
             new_key: T::AccountId,
-            new_key_sig: T::NewOwnerKeySignature,
+            new_key_sig: T::Signature,
         ) -> DispatchResultWithPostInfo {
             // verification phase
             let who = ensure_signed(origin)?;
@@ -461,7 +463,7 @@ pub mod pallet {
                 };
 
             let genesis_hash = frame_system::Pallet::<T>::block_hash(T::BlockNumber::zero());
-            let new_key_payload = NewOwnerKeyPayload {
+            let new_key_payload = IdtyIndexAccountIdPayload {
                 genesis_hash: &genesis_hash,
                 idty_index,
                 old_owner_key: &idty_value.owner_key,
@@ -470,7 +472,7 @@ pub mod pallet {
             ensure!(
                 (NEW_OWNER_KEY_PAYLOAD_PREFIX, new_key_payload)
                     .using_encoded(|bytes| new_key_sig.verify(bytes, &new_key)),
-                Error::<T>::InvalidNewOwnerKeySig
+                Error::<T>::InvalidSignature
             );
 
             // Apply phase
@@ -512,7 +514,7 @@ pub mod pallet {
             origin: OriginFor<T>,
             idty_index: T::IdtyIndex,
             revocation_key: T::AccountId,
-            revocation_sig: T::RevocationSignature,
+            revocation_sig: T::Signature,
         ) -> DispatchResultWithPostInfo {
             let _ = ensure_signed(origin)?;
 
@@ -544,7 +546,7 @@ pub mod pallet {
             ensure!(
                 (REVOCATION_PAYLOAD_PREFIX, revocation_payload)
                     .using_encoded(|bytes| revocation_sig.verify(bytes, &revocation_key)),
-                Error::<T>::InvalidRevocationSig
+                Error::<T>::InvalidSignature
             );
 
             // finally if all checks pass, remove identity
@@ -605,6 +607,39 @@ pub mod pallet {
 
             Ok(().into())
         }
+
+        /// Link an account to an identity
+        // both must sign (target account and identity)
+        // can be used for quota system
+        // re-uses new owner key payload for simplicity
+        // with other custom prefix
+        #[pallet::call_index(8)]
+        #[pallet::weight(T::WeightInfo::link_account())]
+        pub fn link_account(
+            origin: OriginFor<T>,      // origin must have an identity index
+            account_id: T::AccountId,  // id of account to link (must sign the payload)
+            payload_sig: T::Signature, // signature with linked identity
+        ) -> DispatchResultWithPostInfo {
+            // verif
+            let who = ensure_signed(origin)?;
+            let idty_index =
+                IdentityIndexOf::<T>::get(&who).ok_or(Error::<T>::IdtyIndexNotFound)?;
+            let genesis_hash = frame_system::Pallet::<T>::block_hash(T::BlockNumber::zero());
+            let payload = IdtyIndexAccountIdPayload {
+                genesis_hash: &genesis_hash,
+                idty_index,
+                old_owner_key: &account_id,
+            };
+            ensure!(
+                (LINK_IDTY_PAYLOAD_PREFIX, payload)
+                    .using_encoded(|bytes| payload_sig.verify(bytes, &account_id)),
+                Error::<T>::InvalidSignature
+            );
+            // apply
+            Self::do_link_account(account_id, idty_index);
+
+            Ok(().into())
+        }
     }
 
     // ERRORS //
@@ -635,12 +670,10 @@ pub mod pallet {
         IdtyNotValidated,
         /// Identity not yet renewable
         IdtyNotYetRenewable,
-        /// New owner key payload signature is invalid
-        InvalidNewOwnerKeySig,
+        /// payload signature is invalid
+        InvalidSignature,
         /// Revocation key is invalid
         InvalidRevocationKey,
-        /// Revocation payload signature is invalid
-        InvalidRevocationSig,
         /// Identity creation period is not respected
         NotRespectIdtyCreationPeriod,
         /// Not the same identity name
@@ -733,6 +766,12 @@ pub mod pallet {
 
             total_weight
         }
+
+        /// link account
+        fn do_link_account(account_id: T::AccountId, idty_index: T::IdtyIndex) {
+            // call account linker
+            T::AccountLinker::link_identity(account_id, idty_index);
+        }
     }
 }
 
@@ -762,7 +801,7 @@ where
             Default::default()
         }
     }
-    /// mutate an account fiven a function of its data
+    /// mutate an account given a function of its data
     fn try_mutate_exists<R, E: From<sp_runtime::DispatchError>>(
         key: &T::AccountId,
         f: impl FnOnce(&mut Option<T::IdtyData>) -> Result<R, E>,
diff --git a/pallets/identity/src/mock.rs b/pallets/identity/src/mock.rs
index 0238aa00826f805a6f52a58e34c10baf23e955e3..8311e4aa69078a57562a12ce82d278d8eba26fed 100644
--- a/pallets/identity/src/mock.rs
+++ b/pallets/identity/src/mock.rs
@@ -108,13 +108,12 @@ impl pallet_identity::Config for Test {
     type IdtyData = ();
     type IdtyNameValidator = IdtyNameValidatorTestImpl;
     type IdtyIndex = u64;
+    type AccountLinker = ();
     type IdtyRemovalOtherReason = ();
-    type NewOwnerKeySigner = AccountPublic;
-    type NewOwnerKeySignature = Signature;
+    type Signer = AccountPublic;
+    type Signature = Signature;
     type OnIdtyChange = ();
     type RemoveIdentityConsumers = ();
-    type RevocationSigner = AccountPublic;
-    type RevocationSignature = Signature;
     type RuntimeEvent = RuntimeEvent;
     type WeightInfo = ();
     #[cfg(feature = "runtime-benchmarks")]
diff --git a/pallets/identity/src/tests.rs b/pallets/identity/src/tests.rs
index c6b3df952ca50dd23b41603821e10598dfb1ada8..f112fa173c33fe79a9ebb6446bfe9737caaa3953 100644
--- a/pallets/identity/src/tests.rs
+++ b/pallets/identity/src/tests.rs
@@ -16,8 +16,9 @@
 
 use crate::mock::*;
 use crate::{
-    pallet, Error, GenesisIdty, IdtyName, IdtyRemovalReason, IdtyValue, NewOwnerKeyPayload,
-    RevocationPayload, NEW_OWNER_KEY_PAYLOAD_PREFIX, REVOCATION_PAYLOAD_PREFIX,
+    pallet, Error, GenesisIdty, IdtyIndexAccountIdPayload, IdtyName, IdtyRemovalReason, IdtyValue,
+    RevocationPayload, LINK_IDTY_PAYLOAD_PREFIX, NEW_OWNER_KEY_PAYLOAD_PREFIX,
+    REVOCATION_PAYLOAD_PREFIX,
 };
 use codec::Encode;
 use frame_support::dispatch::DispatchResultWithPostInfo;
@@ -158,7 +159,7 @@ fn test_create_identity_but_not_confirm_it() {
 
         System::assert_has_event(RuntimeEvent::Identity(crate::Event::IdtyRemoved {
             idty_index: 2,
-            reason: crate::IdtyRemovalReason::<()>::Expired,
+            reason: IdtyRemovalReason::<()>::Expired,
         }));
 
         // We shoud be able to recreate the identity
@@ -227,7 +228,7 @@ fn test_change_owner_key() {
     .execute_with(|| {
         let genesis_hash = System::block_hash(0);
         let old_owner_key = account(1).id;
-        let mut new_key_payload = NewOwnerKeyPayload {
+        let mut new_key_payload = IdtyIndexAccountIdPayload {
             genesis_hash: &genesis_hash,
             idty_index: 1u64,
             old_owner_key: &old_owner_key,
@@ -261,7 +262,7 @@ fn test_change_owner_key() {
                     (NEW_OWNER_KEY_PAYLOAD_PREFIX, new_key_payload.clone()).encode()
                 )
             ),
-            Error::<Test>::InvalidNewOwnerKeySig
+            Error::<Test>::InvalidSignature
         );
 
         // Payload must be prefixed
@@ -271,7 +272,7 @@ fn test_change_owner_key() {
                 account(10).id,
                 test_signature(account(10).signer, new_key_payload.clone().encode())
             ),
-            Error::<Test>::InvalidNewOwnerKeySig
+            Error::<Test>::InvalidSignature
         );
 
         // New owner key should not be used by another identity
@@ -370,6 +371,70 @@ fn test_change_owner_key() {
     });
 }
 
+// test link identity (does nothing because of AccountLinker type)
+#[test]
+fn test_link_account() {
+    new_test_ext(IdentityConfig {
+        identities: vec![alice(), bob()],
+    })
+    .execute_with(|| {
+        let genesis_hash = System::block_hash(0);
+        let account_id = account(10).id;
+        let payload = IdtyIndexAccountIdPayload {
+            genesis_hash: &genesis_hash,
+            idty_index: 1u64,
+            old_owner_key: &account_id,
+        };
+
+        run_to_block(1);
+
+        // Caller should have an associated identity
+        assert_noop!(
+            Identity::link_account(
+                RuntimeOrigin::signed(account(42).id),
+                account(10).id,
+                test_signature(
+                    account(10).signer,
+                    (LINK_IDTY_PAYLOAD_PREFIX, payload.clone()).encode()
+                )
+            ),
+            Error::<Test>::IdtyIndexNotFound
+        );
+        // Payload must be signed by the new key
+        assert_noop!(
+            Identity::link_account(
+                RuntimeOrigin::signed(account(1).id),
+                account(10).id,
+                test_signature(
+                    account(42).signer,
+                    (LINK_IDTY_PAYLOAD_PREFIX, payload.clone()).encode()
+                )
+            ),
+            Error::<Test>::InvalidSignature
+        );
+
+        // Payload must be prefixed
+        assert_noop!(
+            Identity::link_account(
+                RuntimeOrigin::signed(account(1).id),
+                account(10).id,
+                test_signature(account(10).signer, payload.clone().encode())
+            ),
+            Error::<Test>::InvalidSignature
+        );
+
+        // Alice can call link_account successfully
+        assert_ok!(Identity::link_account(
+            RuntimeOrigin::signed(account(1).id),
+            account(10).id,
+            test_signature(
+                account(10).signer,
+                (LINK_IDTY_PAYLOAD_PREFIX, payload.clone()).encode()
+            )
+        ));
+    });
+}
+
 #[test]
 fn test_idty_revocation_with_old_key() {
     new_test_ext(IdentityConfig {
@@ -377,7 +442,7 @@ fn test_idty_revocation_with_old_key() {
     })
     .execute_with(|| {
         let genesis_hash = System::block_hash(0);
-        let new_key_payload = NewOwnerKeyPayload {
+        let new_key_payload = IdtyIndexAccountIdPayload {
             genesis_hash: &genesis_hash,
             idty_index: 1u64,
             old_owner_key: &account(1).id,
@@ -427,7 +492,7 @@ fn test_idty_revocation_with_old_key_after_old_key_expiration() {
     })
     .execute_with(|| {
         let genesis_hash = System::block_hash(0);
-        let new_key_payload = NewOwnerKeyPayload {
+        let new_key_payload = IdtyIndexAccountIdPayload {
             genesis_hash: &genesis_hash,
             idty_index: 1u64,
             old_owner_key: &account(1).id,
@@ -507,7 +572,7 @@ fn test_idty_revocation() {
                 account(1).id,
                 test_signature(account(1).signer, revocation_payload.encode())
             ),
-            Err(Error::<Test>::InvalidRevocationSig.into())
+            Err(Error::<Test>::InvalidSignature.into())
         );
 
         // Anyone can submit a revocation payload
@@ -526,7 +591,7 @@ fn test_idty_revocation() {
         }));
         System::assert_has_event(RuntimeEvent::Identity(crate::Event::IdtyRemoved {
             idty_index: 1,
-            reason: crate::IdtyRemovalReason::<()>::Revoked,
+            reason: IdtyRemovalReason::<()>::Revoked,
         }));
 
         run_to_block(2);
diff --git a/pallets/identity/src/traits.rs b/pallets/identity/src/traits.rs
index 1bd9a2a4cbd2ece902298e22b65d4f08d845d7b4..86acc604be0fc9e5c2d78a9fff4baffde8d6c079 100644
--- a/pallets/identity/src/traits.rs
+++ b/pallets/identity/src/traits.rs
@@ -77,6 +77,15 @@ impl<IndtyIndex> RemoveIdentityConsumers<IndtyIndex> for () {
     }
 }
 
+/// trait used to link an account to an identity
+pub trait LinkIdty<AccountId, IdtyIndex> {
+    fn link_identity(account_id: AccountId, idty_index: IdtyIndex);
+}
+impl<AccountId, IdtyIndex> LinkIdty<AccountId, IdtyIndex> for () {
+    fn link_identity(_: AccountId, _: IdtyIndex) {}
+}
+
+/// trait used only in benchmarks to prepare identity for benchmarking
 #[cfg(feature = "runtime-benchmarks")]
 pub trait SetupBenchmark<IndtyIndex, AccountId> {
     fn force_status_ok(idty_index: &IndtyIndex, account: &AccountId) -> ();
diff --git a/pallets/identity/src/types.rs b/pallets/identity/src/types.rs
index 786aae6ab1be8794f0cf65c847304821f003b86c..f497ac34d7a8b34e09e609143208949d380d846f 100644
--- a/pallets/identity/src/types.rs
+++ b/pallets/identity/src/types.rs
@@ -26,7 +26,10 @@ use sp_std::vec::Vec;
 /// events related to identity
 pub enum IdtyEvent<T: crate::Config> {
     /// creation of a new identity by an other
-    Created { creator: T::IdtyIndex },
+    Created {
+        creator: T::IdtyIndex,
+        owner_key: T::AccountId,
+    },
     /// confirmation of an identity (with a given name)
     Confirmed,
     /// validation of an identity
@@ -121,9 +124,9 @@ pub struct IdtyValue<BlockNumber, AccountId, IdtyData> {
 
 /// payload to define a new owner key
 #[derive(Clone, Copy, Encode, RuntimeDebug)]
-pub struct NewOwnerKeyPayload<'a, AccountId, IdtyIndex, Hash> {
+pub struct IdtyIndexAccountIdPayload<'a, AccountId, IdtyIndex, Hash> {
     /// hash of the genesis block
-    // Avoid replay attack between networks
+    // Avoid replay attack across networks
     pub genesis_hash: &'a Hash,
     /// identity index
     pub idty_index: IdtyIndex,
@@ -134,7 +137,7 @@ pub struct NewOwnerKeyPayload<'a, AccountId, IdtyIndex, Hash> {
 #[derive(Clone, Copy, Encode, Decode, PartialEq, Eq, TypeInfo, RuntimeDebug)]
 pub struct RevocationPayload<IdtyIndex, Hash> {
     /// hash of the genesis block
-    // Avoid replay attack between networks
+    // Avoid replay attack across networks
     pub genesis_hash: Hash,
     /// identity index
     pub idty_index: IdtyIndex,
diff --git a/pallets/identity/src/weights.rs b/pallets/identity/src/weights.rs
index 0f1246f69e98b0ebdc0cb24c3a72434eb9b17e9a..f1058b770c2311e3eba73d798785eaa017c9778a 100644
--- a/pallets/identity/src/weights.rs
+++ b/pallets/identity/src/weights.rs
@@ -28,6 +28,7 @@ pub trait WeightInfo {
     fn remove_identity() -> Weight;
     fn prune_item_identities_names(i: u32) -> Weight;
     fn fix_sufficients() -> Weight;
+    fn link_account() -> Weight;
 }
 
 // Insecure weights implementation, use it for tests only!
@@ -135,4 +136,20 @@ impl WeightInfo for () {
             .saturating_add(RocksDbWeight::get().reads(1 as u64))
             .saturating_add(RocksDbWeight::get().writes(1 as u64))
     }
+    /// Storage: Identity IdentityIndexOf (r:1 w:0)
+    /// Proof Skipped: Identity IdentityIndexOf (max_values: None, max_size: None, mode: Measured)
+    /// Storage: System BlockHash (r:1 w:0)
+    /// Proof: System BlockHash (max_values: None, max_size: Some(44), added: 2519, mode: MaxEncodedLen)
+    /// Storage: System Account (r:1 w:1)
+    /// Proof: System Account (max_values: None, max_size: Some(126), added: 2601, mode: MaxEncodedLen)
+    fn link_account() -> Weight {
+        // Proof Size summary in bytes:
+        //  Measured:  `359`
+        //  Estimated: `3824`
+        // Minimum execution time: 543_046_000 picoseconds.
+        Weight::from_parts(544_513_000, 0)
+            .saturating_add(Weight::from_parts(0, 3824))
+            .saturating_add(RocksDbWeight::get().reads(3))
+            .saturating_add(RocksDbWeight::get().writes(1))
+    }
 }
diff --git a/pallets/membership/src/lib.rs b/pallets/membership/src/lib.rs
index 73a7e10a84450b034b6a55228bde8b77a643a6a0..1b7a2d07e017db47a75e68955e81beeeee9edd85 100644
--- a/pallets/membership/src/lib.rs
+++ b/pallets/membership/src/lib.rs
@@ -233,9 +233,11 @@ pub mod pallet {
             Self::do_request_membership(idty_id, metadata)
         }
 
-        /// claim pending membership to become actual memberhip
-        /// the requested membership must fullfill requirements
-        // for main wot claim_membership is called automatically when validating identity
+        /// claim membership  
+        /// a pending membership should exist  
+        /// it must fullfill the requirements (certs, distance)  
+        /// for main wot claim_membership is called automatically when validating identity  
+        /// for smith wot, it means joining the authority members  
         #[pallet::call_index(1)]
         #[pallet::weight(T::WeightInfo::claim_membership())]
         pub fn claim_membership(origin: OriginFor<T>) -> DispatchResultWithPostInfo {
diff --git a/pallets/quota/Cargo.toml b/pallets/quota/Cargo.toml
new file mode 100644
index 0000000000000000000000000000000000000000..2c96eed66ca14093588744b5fe7e0f0ba59a56a0
--- /dev/null
+++ b/pallets/quota/Cargo.toml
@@ -0,0 +1,77 @@
+[package]
+authors = ['HugoTrentesaux <hugo@trentesaux.fr>']
+description = 'duniter pallet quota'
+edition = "2021"
+homepage = 'https://duniter.org'
+license = 'AGPL-3.0'
+name = 'pallet-quota'
+readme = 'README.md'
+repository = 'https://git.duniter.org/nodes/rust/duniter-v2s'
+version = '3.0.0'
+
+[features]
+default = ['std']
+runtime-benchmarks = ['frame-benchmarking/runtime-benchmarks']
+std = [
+    'codec/std',
+    'frame-support/std',
+    'frame-system/std',
+    'frame-benchmarking/std',
+    'sp-core/std',
+    'sp-runtime/std',
+    'sp-std/std',
+    'pallet-identity/std',
+    'pallet-balances/std',
+]
+try-runtime = ['frame-support/try-runtime']
+
+[package.metadata.docs.rs]
+targets = ['x86_64-unknown-linux-gnu']
+
+[dependencies]
+pallet-identity = { path = "../identity", default-features = false }
+
+# crates.io
+codec = { package = 'parity-scale-codec', version = "3.1.5", features = ['derive'], default-features = false }
+scale-info = { version = "2.1.1", default-features = false, features = ["derive"] }
+
+# substrate
+
+[dependencies.pallet-balances]
+default-features = false
+git = 'https://github.com/duniter/substrate'
+branch = 'duniter-substrate-v0.9.42'
+
+[dependencies.frame-benchmarking]
+default-features = false
+git = 'https://github.com/duniter/substrate'
+optional = true
+branch = 'duniter-substrate-v0.9.42'
+
+[dependencies.frame-support]
+default-features = false
+git = 'https://github.com/duniter/substrate'
+branch = 'duniter-substrate-v0.9.42'
+
+[dependencies.frame-system]
+default-features = false
+git = 'https://github.com/duniter/substrate'
+branch = 'duniter-substrate-v0.9.42'
+
+[dependencies.sp-core]
+default-features = false
+git = 'https://github.com/duniter/substrate'
+branch = 'duniter-substrate-v0.9.42'
+
+[dependencies.sp-runtime]
+default-features = false
+git = 'https://github.com/duniter/substrate'
+branch = 'duniter-substrate-v0.9.42'
+
+[dependencies.sp-std]
+default-features = false
+git = 'https://github.com/duniter/substrate'
+branch = 'duniter-substrate-v0.9.42'
+
+[dev-dependencies]
+sp-io = { git = 'https://github.com/duniter/substrate', branch = 'duniter-substrate-v0.9.42' }
diff --git a/pallets/quota/README.md b/pallets/quota/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..6ed526208db7b13ab2ebda4983c76778aeaff853
--- /dev/null
+++ b/pallets/quota/README.md
@@ -0,0 +1,31 @@
+# Duniter quota pallet
+
+Duniter identity system allows to allocate quota and refund transaction fees when not consumed.
+
+## General behavior
+
+Quota system is plugged to transactions fees which is a rather critical aspect of substrate.
+That's why in `duniter-account` pallet `OnChargeTransaction` implementation, the default behavior is preserved, and refunds are added to a queue handeled in `on_idle`.
+
+## Path for a refund
+
+This is what happens on a transaction:
+
+- `frame-executive` calls `OnChargeTransaction` implementations
+- `duniter-account` `OnChargeTransaction` implementation is called, and if an identity is linked to the account who pays the fees, `request_refund` is called
+- `request_refund` implementation of `quota` pallet determines whether the fees are eligible for refund based on the identity and then call `queue_refund`
+- `queue_refund` adds a refund to the `RefundQueue` which will be processed in `on_idle`
+- during `on_idle`, `quota` pallet processes the refund queue within the supplied weight limit with `process_refund_queue`
+- for each refund in the `RefundQueue`, `try_refund` is called
+- it first tries to use quotas to refund fees with `spend_quota`
+- if a certain amount of quotas has been spend, it actually performs the refund with `do_refund`, taking currency from the `RefundAccount` to give it back to the account who paid the fee
+
+The conditions for a refund to happen are:
+
+1. an identity is linked to the account who pays the fees
+1. some quotas are defined for the identity and have a non-null value after update
+
+
+## TODO
+
+- [ ] sanity test checking that only member identities have quota
\ No newline at end of file
diff --git a/pallets/quota/src/benchmarking.rs b/pallets/quota/src/benchmarking.rs
new file mode 100644
index 0000000000000000000000000000000000000000..587ff9cc52a65bdf7649df14aabf77979c6f3cb8
--- /dev/null
+++ b/pallets/quota/src/benchmarking.rs
@@ -0,0 +1,64 @@
+// Copyright 2021-2023 Axiom-Team
+//
+// This file is part of Duniter-v2S.
+//
+// Duniter-v2S is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, version 3 of the License.
+//
+// Duniter-v2S 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 Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with Duniter-v2S. If not, see <https://www.gnu.org/licenses/>.
+
+#![cfg(feature = "runtime-benchmarks")]
+
+use super::*;
+use frame_benchmarking::{account, benchmarks};
+
+// FIXME this is a naïve implementation of benchmarks:
+// - without properly prepare data
+// - without "verify" blocks
+// - without thinking about worst case scenario
+// - without writing complexity in the term of refund queue length
+// It's there as a seed for benchmark implementation and to use WeightInfo where needed.
+
+benchmarks! {
+    where_clause {
+        where
+            IdtyId<T>: From<u32>,
+            BalanceOf<T>: From<u64>,
+    }
+    queue_refund {
+        let account: T::AccountId = account("Alice", 1, 1);
+        let refund = Refund {
+            account,
+            identity: 1u32.into(),
+            amount: 10u64.into(),
+        };
+    }: { Pallet::<T>::queue_refund(refund) }
+    spend_quota {
+        let idty_id = 1u32;
+        let amount = 1u64;
+    }: { Pallet::<T>::spend_quota(idty_id.into(), amount.into()) }
+    try_refund {
+        let account: T::AccountId = account("Alice", 1, 1);
+        let refund = Refund {
+            account,
+            identity: 1u32.into(),
+            amount: 10u64.into(),
+        };
+    }: { Pallet::<T>::try_refund(refund) }
+    do_refund {
+        let account: T::AccountId = account("Alice", 1, 1);
+        let refund = Refund {
+            account,
+            identity: 1u32.into(),
+            amount: 10u64.into(),
+        };
+        let amount = 5u64.into();
+    }: { Pallet::<T>::do_refund(refund, amount) }
+}
diff --git a/pallets/quota/src/lib.rs b/pallets/quota/src/lib.rs
new file mode 100644
index 0000000000000000000000000000000000000000..eff611f21653cebc441ef6dba6d76fd4b772abfd
--- /dev/null
+++ b/pallets/quota/src/lib.rs
@@ -0,0 +1,361 @@
+// Copyright 2021-2023 Axiom-Team
+//
+// This file is part of Duniter-v2S.
+//
+// Duniter-v2S is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, version 3 of the License.
+//
+// Duniter-v2S 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 Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with Duniter-v2S. If not, see <https://www.gnu.org/licenses/>.
+
+#![cfg_attr(not(feature = "std"), no_std)]
+
+pub mod traits;
+pub mod weights;
+
+#[cfg(test)]
+mod mock;
+
+#[cfg(test)]
+mod tests;
+
+#[cfg(feature = "runtime-benchmarks")]
+pub mod benchmarking;
+
+use crate::traits::*;
+use frame_support::pallet_prelude::*;
+use frame_support::traits::{Currency, ExistenceRequirement};
+use frame_system::pallet_prelude::*;
+pub use pallet::*;
+use pallet_identity::IdtyEvent;
+use sp_runtime::traits::Zero;
+use sp_std::fmt::Debug;
+pub use weights::WeightInfo;
+
+#[frame_support::pallet]
+pub mod pallet {
+    use super::*;
+
+    pub const MAX_QUEUED_REFUNDS: u32 = 256;
+
+    // Currency used for quota is the one of pallet balances
+    pub type CurrencyOf<T> = pallet_balances::Pallet<T>;
+    // Balance used for quota is the one associated to balance currency
+    pub type BalanceOf<T> =
+        <CurrencyOf<T> as Currency<<T as frame_system::Config>::AccountId>>::Balance;
+    // identity id is pallet identity idty_index
+    pub type IdtyId<T> = <T as pallet_identity::Config>::IdtyIndex;
+
+    #[pallet::pallet]
+    pub struct Pallet<T>(_);
+
+    // CONFIG //
+    #[pallet::config]
+    pub trait Config:
+        frame_system::Config + pallet_balances::Config + pallet_identity::Config
+    {
+        /// Because this pallet emits events, it depends on the runtime's definition of an event.
+        type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
+        /// number of blocks in which max quota is replenished
+        type ReloadRate: Get<Self::BlockNumber>;
+        /// maximum amount of quota an identity can get
+        type MaxQuota: Get<BalanceOf<Self>>;
+        /// Account used to refund fee
+        #[pallet::constant]
+        type RefundAccount: Get<Self::AccountId>;
+        /// Weight
+        type WeightInfo: WeightInfo;
+    }
+
+    // TYPES //
+    #[derive(Encode, Decode, Clone, TypeInfo, Debug, PartialEq, MaxEncodedLen)]
+    pub struct Refund<AccountId, IdtyId, Balance> {
+        /// account to refund
+        pub account: AccountId,
+        /// identity to use quota
+        pub identity: IdtyId,
+        /// amount of refund
+        pub amount: Balance,
+    }
+
+    #[derive(Encode, Decode, Clone, TypeInfo, Debug, PartialEq, MaxEncodedLen)]
+    pub struct Quota<BlockNumber, Balance> {
+        /// block number of last quota use
+        pub last_use: BlockNumber,
+        /// amount of remaining quota
+        pub amount: Balance,
+    }
+
+    // STORAGE //
+    /// maps identity index to quota
+    #[pallet::storage]
+    #[pallet::getter(fn quota)]
+    pub type IdtyQuota<T: Config> =
+        StorageMap<_, Twox64Concat, IdtyId<T>, Quota<T::BlockNumber, BalanceOf<T>>, OptionQuery>;
+
+    /// fees waiting for refund
+    #[pallet::storage]
+    pub type RefundQueue<T: Config> = StorageValue<
+        _,
+        BoundedVec<Refund<T::AccountId, IdtyId<T>, BalanceOf<T>>, ConstU32<MAX_QUEUED_REFUNDS>>,
+        ValueQuery,
+    >;
+
+    // EVENTS //
+    #[pallet::event]
+    #[pallet::generate_deposit(pub(super) fn deposit_event)]
+    pub enum Event<T: Config> {
+        /// Refunded fees to an account
+        Refunded {
+            who: T::AccountId,
+            identity: IdtyId<T>,
+            amount: BalanceOf<T>,
+        },
+        // --- the following events let know that an error occured ---
+        /// No quota for identity
+        NoQuotaForIdty(IdtyId<T>),
+        /// No more currency available for refund
+        // should never happen if the fees are going to the refund account
+        NoMoreCurrencyForRefund,
+        /// Refund failed
+        // for example when account is destroyed
+        RefundFailed(T::AccountId),
+        /// Refund queue full
+        RefundQueueFull,
+    }
+
+    // // ERRORS //
+    // #[pallet::error]
+    // pub enum Error<T> {
+    //     // no errors in on_idle
+    //     // instead events are emitted
+    // }
+
+    // // CALLS //
+    // #[pallet::call]
+    // impl<T: Config> Pallet<T> {
+    //     // no calls for this pallet, only automatic processing when idle
+    // }
+
+    // INTERNAL FUNCTIONS //
+    impl<T: Config> Pallet<T> {
+        /// add a new refund to the queue
+        pub fn queue_refund(refund: Refund<T::AccountId, IdtyId<T>, BalanceOf<T>>) {
+            if RefundQueue::<T>::mutate(|v| v.try_push(refund)).is_err() {
+                Self::deposit_event(Event::RefundQueueFull);
+            }
+        }
+
+        /// try to refund using quota if available
+        pub fn try_refund(queued_refund: Refund<T::AccountId, IdtyId<T>, BalanceOf<T>>) -> Weight {
+            // get the amount of quota that identity is able to spend
+            let amount = Self::spend_quota(queued_refund.identity, queued_refund.amount);
+            if amount.is_zero() {
+                // partial weight
+                return <T as pallet::Config>::WeightInfo::spend_quota();
+            }
+            // only perform refund if amount is not null
+            Self::do_refund(queued_refund, amount);
+            // total weight
+            <T as pallet::Config>::WeightInfo::spend_quota()
+                .saturating_add(<T as pallet::Config>::WeightInfo::do_refund())
+        }
+
+        /// do refund a non-null amount
+        // opti: more accurate estimation of consumed weight
+        pub fn do_refund(
+            queued_refund: Refund<T::AccountId, IdtyId<T>, BalanceOf<T>>,
+            amount: BalanceOf<T>,
+        ) {
+            // take money from refund account
+            let res = CurrencyOf::<T>::withdraw(
+                &T::RefundAccount::get(),
+                amount,
+                frame_support::traits::WithdrawReasons::FEE, // a fee but in reverse
+                ExistenceRequirement::KeepAlive,
+            );
+            // if successful
+            if let Ok(imbalance) = res {
+                // perform refund
+                let res = CurrencyOf::<T>::resolve_into_existing(&queued_refund.account, imbalance);
+                match res {
+                    // take money from refund account OK + refund account OK → event
+                    Ok(_) => {
+                        Self::deposit_event(Event::Refunded {
+                            who: queued_refund.account,
+                            identity: queued_refund.identity,
+                            amount,
+                        });
+                    }
+                    Err(imbalance) => {
+                        // refund failed (for example account stopped existing) → handle dust
+                        // give back to refund account (should not happen)
+                        CurrencyOf::<T>::resolve_creating(&T::RefundAccount::get(), imbalance);
+                        // if this event is observed, block should be examined carefully
+                        Self::deposit_event(Event::RefundFailed(queued_refund.account));
+                    }
+                }
+            } else {
+                // could not withdraw refund account
+                Self::deposit_event(Event::NoMoreCurrencyForRefund);
+            }
+        }
+
+        /// perform as many refunds as possible within the supplied weight limit
+        pub fn process_refund_queue(weight_limit: Weight) -> Weight {
+            RefundQueue::<T>::mutate(|queue| {
+                if queue.is_empty() {
+                    return Weight::zero();
+                }
+                let mut total_weight = Weight::zero();
+                // make sure that we have at least the time to handle one try_refund call
+                while total_weight.any_lt(
+                    weight_limit.saturating_sub(<T as pallet::Config>::WeightInfo::try_refund()),
+                ) {
+                    let Some(queued_refund) = queue.pop() else {
+                        break;
+                    };
+                    let consumed_weight = Self::try_refund(queued_refund);
+                    total_weight = total_weight.saturating_add(consumed_weight);
+                }
+                total_weight
+            })
+        }
+
+        /// spend quota of identity
+        pub fn spend_quota(idty_id: IdtyId<T>, amount: BalanceOf<T>) -> BalanceOf<T> {
+            IdtyQuota::<T>::mutate_exists(idty_id, |quota| {
+                if let Some(ref mut quota) = quota {
+                    Self::update_quota(quota);
+                    Self::do_spend_quota(quota, amount)
+                } else {
+                    // error event if identity has no quota
+                    Self::deposit_event(Event::NoQuotaForIdty(idty_id));
+                    BalanceOf::<T>::zero()
+                }
+            })
+        }
+
+        /// update quota according to the growth rate, max value, and last use
+        fn update_quota(quota: &mut Quota<T::BlockNumber, BalanceOf<T>>) {
+            let current_block = frame_system::pallet::Pallet::<T>::block_number();
+            let quota_growth = sp_runtime::Perbill::from_rational(
+                current_block - quota.last_use,
+                T::ReloadRate::get(),
+            )
+            .mul_floor(T::MaxQuota::get());
+            // mutate quota
+            quota.last_use = current_block;
+            quota.amount = core::cmp::min(quota.amount + quota_growth, T::MaxQuota::get());
+        }
+        /// spend a certain amount of quota and return what was spent
+        fn do_spend_quota(
+            quota: &mut Quota<T::BlockNumber, BalanceOf<T>>,
+            amount: BalanceOf<T>,
+        ) -> BalanceOf<T> {
+            let old_amount = quota.amount;
+            // entire amount fit in remaining quota
+            if amount <= old_amount {
+                quota.amount -= amount;
+                amount
+            }
+            // all quota are spent and only partial refund is possible
+            else {
+                quota.amount = BalanceOf::<T>::zero();
+                old_amount
+            }
+        }
+    }
+
+    // GENESIS STUFF //
+    #[pallet::genesis_config]
+    pub struct GenesisConfig<T: Config> {
+        pub identities: Vec<IdtyId<T>>,
+    }
+
+    #[cfg(feature = "std")]
+    impl<T: Config> Default for GenesisConfig<T> {
+        fn default() -> Self {
+            Self {
+                identities: Default::default(),
+            }
+        }
+    }
+
+    #[pallet::genesis_build]
+    impl<T: Config> GenesisBuild<T> for GenesisConfig<T> {
+        fn build(&self) {
+            for idty in self.identities.iter() {
+                IdtyQuota::<T>::insert(
+                    idty,
+                    Quota {
+                        last_use: T::BlockNumber::zero(),
+                        amount: BalanceOf::<T>::zero(),
+                    },
+                );
+            }
+        }
+    }
+
+    // HOOKS //
+    #[pallet::hooks]
+    impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
+        // process refund queue if space left on block
+        fn on_idle(_block: T::BlockNumber, remaining_weight: Weight) -> Weight {
+            Self::process_refund_queue(remaining_weight)
+            // opti: benchmark process_refund_queue overhead and substract this from weight limit
+            // .saturating_sub(T::WeightInfo::process_refund_queue())
+        }
+    }
+}
+
+// implement quota traits
+impl<T: Config> RefundFee<T> for Pallet<T> {
+    fn request_refund(account: T::AccountId, identity: IdtyId<T>, amount: BalanceOf<T>) {
+        if is_eligible_for_refund::<T>(identity) {
+            Self::queue_refund(Refund {
+                account,
+                identity,
+                amount,
+            })
+        }
+    }
+}
+
+/// tells whether an identity is eligible for refund
+fn is_eligible_for_refund<T: pallet_identity::Config>(_identity: IdtyId<T>) -> bool {
+    // all identities are eligible for refund, no matter their status
+    // if the identity has no quotas or has been deleted, the refund request is still queued
+    // but when handeled, no refund will be issued (and `NoQuotaForIdty` may be raised)
+    true
+}
+
+// implement identity event handler
+impl<T: Config> pallet_identity::traits::OnIdtyChange<T> for Pallet<T> {
+    fn on_idty_change(idty_id: IdtyId<T>, idty_event: &IdtyEvent<T>) -> Weight {
+        match idty_event {
+            // initialize quota on identity creation
+            IdtyEvent::Created { .. } => {
+                IdtyQuota::<T>::insert(
+                    idty_id,
+                    Quota {
+                        last_use: frame_system::pallet::Pallet::<T>::block_number(),
+                        amount: BalanceOf::<T>::zero(),
+                    },
+                );
+            }
+            IdtyEvent::Removed { .. } => {
+                IdtyQuota::<T>::remove(idty_id);
+            }
+            IdtyEvent::Confirmed | IdtyEvent::Validated | IdtyEvent::ChangedOwnerKey { .. } => {}
+        }
+        // TODO proper weight
+        Weight::zero()
+    }
+}
diff --git a/pallets/quota/src/mock.rs b/pallets/quota/src/mock.rs
new file mode 100644
index 0000000000000000000000000000000000000000..17b825fc4e12e0a2a707c69577a0d1f922d81ed0
--- /dev/null
+++ b/pallets/quota/src/mock.rs
@@ -0,0 +1,189 @@
+// Copyright 2021 Axiom-Team
+//
+// This file is part of Duniter-v2S.
+//
+// Duniter-v2S is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, version 3 of the License.
+//
+// Duniter-v2S 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 Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with Duniter-v2S. If not, see <https://www.gnu.org/licenses/>.
+
+// Note: most of this file is copy pasted from common pallet_config and other mocks
+
+use super::*;
+pub use crate::pallet as pallet_quota;
+use frame_support::{
+    parameter_types,
+    traits::{Everything, OnFinalize, OnInitialize},
+};
+use frame_system as system;
+use sp_core::{Pair, H256};
+use sp_runtime::traits::IdentifyAccount;
+use sp_runtime::traits::Verify;
+use sp_runtime::BuildStorage;
+use sp_runtime::{
+    testing::Header,
+    traits::{BlakeTwo256, IdentityLookup},
+    MultiSignature, MultiSigner,
+};
+
+type BlockNumber = u64;
+type Balance = u64;
+type Block = frame_system::mocking::MockBlock<Test>;
+type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic<Test>;
+pub type Signature = MultiSignature;
+pub type AccountPublic = <Signature as Verify>::Signer;
+pub type AccountId = <AccountPublic as IdentifyAccount>::AccountId;
+
+pub fn account(id: u8) -> AccountId {
+    let pair = sp_core::sr25519::Pair::from_seed(&[id; 32]);
+    MultiSigner::Sr25519(pair.public()).into_account()
+}
+
+// Configure a mock runtime to test the pallet.
+frame_support::construct_runtime!(
+    pub enum Test where
+        Block = Block,
+        NodeBlock = Block,
+        UncheckedExtrinsic = UncheckedExtrinsic,
+    {
+        System: frame_system::{Pallet, Call, Config, Storage, Event<T>},
+        Quota: pallet_quota::{Pallet, Storage, Config<T>, Event<T>},
+        Balances: pallet_balances::{Pallet, Call, Storage, Config<T>, Event<T>},
+        Identity: pallet_identity::{Pallet, Call, Storage, Config<T>, Event<T>},
+    }
+);
+
+// QUOTA //
+pub struct TreasuryAccountId;
+impl frame_support::pallet_prelude::Get<AccountId> for TreasuryAccountId {
+    fn get() -> AccountId {
+        account(99)
+    }
+}
+parameter_types! {
+    pub const ReloadRate: u64 = 10;
+    pub const MaxQuota: u64 = 1000;
+}
+impl Config for Test {
+    type RuntimeEvent = RuntimeEvent;
+    type ReloadRate = ReloadRate;
+    type MaxQuota = MaxQuota;
+    type RefundAccount = TreasuryAccountId;
+    type WeightInfo = ();
+}
+
+// SYSTEM //
+parameter_types! {
+    pub const BlockHashCount: u64 = 250;
+    pub const SS58Prefix: u8 = 42;
+}
+impl system::Config for Test {
+    type BaseCallFilter = Everything;
+    type BlockWeights = ();
+    type BlockLength = ();
+    type DbWeight = ();
+    type RuntimeOrigin = RuntimeOrigin;
+    type RuntimeCall = RuntimeCall;
+    type Index = u64;
+    type BlockNumber = BlockNumber;
+    type Hash = H256;
+    type Hashing = BlakeTwo256;
+    type AccountId = AccountId;
+    type Lookup = IdentityLookup<Self::AccountId>;
+    type Header = Header;
+    type RuntimeEvent = RuntimeEvent;
+    type BlockHashCount = BlockHashCount;
+    type Version = ();
+    type PalletInfo = PalletInfo;
+    type AccountData = pallet_balances::AccountData<Balance>;
+    type OnNewAccount = ();
+    type OnKilledAccount = ();
+    type SystemWeightInfo = ();
+    type SS58Prefix = SS58Prefix;
+    type OnSetCode = ();
+    type MaxConsumers = frame_support::traits::ConstU32<16>;
+}
+
+// BALANCES //
+parameter_types! {
+    pub const ExistentialDeposit: Balance = 1000;
+    pub const MaxLocks: u32 = 50;
+}
+impl pallet_balances::Config for Test {
+    type Balance = Balance;
+    type DustRemoval = ();
+    type ExistentialDeposit = ExistentialDeposit;
+    type AccountStore = System;
+    type WeightInfo = pallet_balances::weights::SubstrateWeight<Test>;
+    type MaxLocks = MaxLocks;
+    type MaxReserves = ();
+    type ReserveIdentifier = [u8; 8];
+    type RuntimeEvent = RuntimeEvent;
+    type HoldIdentifier = ();
+    type FreezeIdentifier = ();
+    type MaxHolds = ConstU32<0>;
+    type MaxFreezes = ConstU32<0>;
+}
+
+// IDENTITY //
+parameter_types! {
+    pub const ChangeOwnerKeyPeriod: u64 = 10;
+    pub const ConfirmPeriod: u64 = 2;
+    pub const IdtyCreationPeriod: u64 = 3;
+    pub const MaxInactivityPeriod: u64 = 5;
+    pub const ValidationPeriod: u64 = 2;
+}
+pub struct IdtyNameValidatorTestImpl;
+impl pallet_identity::traits::IdtyNameValidator for IdtyNameValidatorTestImpl {
+    fn validate(idty_name: &pallet_identity::IdtyName) -> bool {
+        idty_name.0.len() < 16
+    }
+}
+impl pallet_identity::Config for Test {
+    type ChangeOwnerKeyPeriod = ChangeOwnerKeyPeriod;
+    type ConfirmPeriod = ConfirmPeriod;
+    type CheckIdtyCallAllowed = ();
+    type IdtyCreationPeriod = IdtyCreationPeriod;
+    type IdtyData = ();
+    type IdtyNameValidator = IdtyNameValidatorTestImpl;
+    type IdtyIndex = u64;
+    type AccountLinker = ();
+    type IdtyRemovalOtherReason = ();
+    type Signer = AccountPublic;
+    type Signature = Signature;
+    type OnIdtyChange = ();
+    type RemoveIdentityConsumers = ();
+    type RuntimeEvent = RuntimeEvent;
+    type WeightInfo = ();
+}
+
+// Build genesis storage according to the mock runtime.
+pub fn new_test_ext(gen_conf: pallet_quota::GenesisConfig<Test>) -> sp_io::TestExternalities {
+    GenesisConfig {
+        system: SystemConfig::default(),
+        balances: BalancesConfig::default(),
+        quota: gen_conf,
+        identity: IdentityConfig::default(),
+    }
+    .build_storage()
+    .unwrap()
+    .into()
+}
+
+pub fn run_to_block(n: BlockNumber) {
+    while System::block_number() < n {
+        <frame_system::Pallet<Test> as OnFinalize<BlockNumber>>::on_finalize(System::block_number());
+        System::reset_events();
+        System::set_block_number(System::block_number() + 1);
+        <frame_system::Pallet<Test> as OnInitialize<BlockNumber>>::on_initialize(
+            System::block_number(),
+        );
+    }
+}
diff --git a/pallets/quota/src/tests.rs b/pallets/quota/src/tests.rs
new file mode 100644
index 0000000000000000000000000000000000000000..14fa890f7f1b338c21ebda6cfabfceda3413fb77
--- /dev/null
+++ b/pallets/quota/src/tests.rs
@@ -0,0 +1,241 @@
+// Copyright 2021 Axiom-Team
+//
+// This file is part of Duniter-v2S.
+//
+// Duniter-v2S is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, version 3 of the License.
+//
+// Duniter-v2S 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 Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with Duniter-v2S. If not, see <https://www.gnu.org/licenses/>.
+
+use crate::mock::*;
+use crate::Weight;
+use frame_support::traits::Currency;
+use sp_core::Get;
+
+// Note: values for reload rate and max quota defined in mock file
+// parameter_types! {
+//     pub const ReloadRate: u64 = 10;
+//     pub const MaxQuota: u64 = 1000;
+// }
+// pub const ExistentialDeposit: Balance = 1000;
+
+/// test that quota are well initialized for genesis identities
+#[test]
+fn test_initial_quota() {
+    new_test_ext(QuotaConfig {
+        identities: vec![1, 2, 3],
+    })
+    .execute_with(|| {
+        run_to_block(1);
+
+        // quota initialized to 0,0 for a given identity
+        assert_eq!(
+            Quota::quota(1),
+            Some(pallet_quota::Quota {
+                last_use: 0,
+                amount: 0
+            })
+        );
+        // no initialized quota for standard account
+        assert_eq!(Quota::quota(4), None);
+    })
+}
+
+/// test that quota are updated according to the reload rate and max quota values
+#[test]
+fn test_update_quota() {
+    new_test_ext(QuotaConfig {
+        identities: vec![1, 2, 3],
+    })
+    .execute_with(|| {
+        // Block 1
+        run_to_block(1);
+        assert_eq!(
+            Quota::quota(1),
+            Some(pallet_quota::Quota {
+                last_use: 0,
+                amount: 0
+            })
+        );
+        // (spending 0 quota will lead to only update)
+        // assert zero quota spent
+        assert_eq!(Quota::spend_quota(1, 0), 0);
+        assert_eq!(
+            Quota::quota(1),
+            Some(pallet_quota::Quota {
+                last_use: 1, // used at block 1
+                // max quota × (current block - last use) / reload rate
+                amount: 100 // 1000 × 1 / 10 = 100
+            })
+        );
+
+        // Block 2
+        run_to_block(2);
+        assert_eq!(Quota::spend_quota(2, 0), 0);
+        assert_eq!(
+            Quota::quota(2),
+            Some(pallet_quota::Quota {
+                last_use: 2, // used at block 2
+                // max quota × (current block - last use) / reload rate
+                amount: 200 // 1000 × 2 / 10 = 200
+            })
+        );
+
+        // Block 20
+        run_to_block(20);
+        assert_eq!(Quota::spend_quota(2, 0), 0);
+        assert_eq!(
+            Quota::quota(2),
+            Some(pallet_quota::Quota {
+                last_use: 20, // used at block 20
+                // maximum quota is reached
+                // 1000 × (20 - 2) / 10 = 1800
+                amount: 1000 // min(1000, 1800)
+            })
+        );
+    })
+}
+
+/// test that right amount of quota is spent
+#[test]
+fn test_spend_quota() {
+    new_test_ext(QuotaConfig {
+        identities: vec![1, 2, 3],
+    })
+    .execute_with(|| {
+        // at block 5, quota are half loaded (500)
+        run_to_block(5);
+        // spending less than available
+        assert_eq!(Quota::spend_quota(1, 200), 200);
+        assert_eq!(
+            Quota::quota(1),
+            Some(pallet_quota::Quota {
+                last_use: 5,
+                amount: 300 // 500 - 200
+            })
+        );
+        // spending all available
+        assert_eq!(Quota::spend_quota(2, 500), 500);
+        assert_eq!(
+            Quota::quota(2),
+            Some(pallet_quota::Quota {
+                last_use: 5,
+                amount: 0 // 500 - 500
+            })
+        );
+        // spending more than available
+        assert_eq!(Quota::spend_quota(3, 1000), 500);
+        assert_eq!(
+            Quota::quota(3),
+            Some(pallet_quota::Quota {
+                last_use: 5,
+                amount: 0 // 500 - 500
+            })
+        );
+    })
+}
+
+/// test complete scenario with queue and process refund queue
+#[test]
+fn test_process_refund_queue() {
+    new_test_ext(QuotaConfig {
+        identities: vec![1, 2],
+    })
+    .execute_with(|| {
+        run_to_block(5);
+        // give enough currency to accounts and treasury and double check
+        Balances::make_free_balance_be(&account(1), 1000);
+        Balances::make_free_balance_be(&account(2), 1000);
+        Balances::make_free_balance_be(&account(3), 1000);
+        Balances::make_free_balance_be(
+            &<Test as pallet_quota::Config>::RefundAccount::get(),
+            10_000,
+        );
+        assert_eq!(
+            Balances::free_balance(<Test as pallet_quota::Config>::RefundAccount::get()),
+            10_000
+        );
+        // fill in the refund queue
+        Quota::queue_refund(pallet_quota::Refund {
+            account: account(1),
+            identity: 1,
+            amount: 10,
+        });
+        Quota::queue_refund(pallet_quota::Refund {
+            account: account(2),
+            identity: 2,
+            amount: 1000,
+        });
+        Quota::queue_refund(pallet_quota::Refund {
+            account: account(3),
+            identity: 3,
+            amount: 666,
+        });
+        // process it
+        Quota::process_refund_queue(Weight::from(10));
+        // after processing, it should be empty
+        assert!(pallet_quota::RefundQueue::<Test>::get().is_empty());
+        // and we should observe the effects of refund
+        assert_eq!(Balances::free_balance(account(1)), 1010); // 1000 initial + 10 refunded
+        assert_eq!(Balances::free_balance(account(2)), 1500); // 1000 initial + 500 refunded
+        assert_eq!(Balances::free_balance(account(3)), 1000); // only initial because no available quota
+        assert_eq!(
+            Balances::free_balance(<Test as pallet_quota::Config>::RefundAccount::get()),
+            // initial minus refunds
+            10_000 - 500 - 10
+        );
+        // events
+        System::assert_has_event(RuntimeEvent::Quota(pallet_quota::Event::Refunded {
+            who: account(1),
+            identity: 1,
+            amount: 10,
+        }));
+        System::assert_has_event(RuntimeEvent::Quota(pallet_quota::Event::NoQuotaForIdty(3)));
+    })
+}
+
+/// test not enough currency in treasury
+#[test]
+fn test_not_enough_treasury() {
+    new_test_ext(QuotaConfig {
+        identities: vec![1],
+    })
+    .execute_with(|| {
+        run_to_block(5);
+        Balances::make_free_balance_be(&account(1), 1000);
+        Balances::make_free_balance_be(&<Test as pallet_quota::Config>::RefundAccount::get(), 1200);
+        Quota::queue_refund(pallet_quota::Refund {
+            account: account(1),
+            identity: 1,
+            amount: 500,
+        });
+        Quota::process_refund_queue(Weight::from(10));
+        // refund was not possible, would kill treasury
+        assert_eq!(Balances::free_balance(account(1)), 1000);
+        assert_eq!(
+            Balances::free_balance(<Test as pallet_quota::Config>::RefundAccount::get()),
+            1200
+        );
+        // event
+        System::assert_has_event(RuntimeEvent::Quota(
+            pallet_quota::Event::NoMoreCurrencyForRefund,
+        ));
+        // quotas were spent anyway, there is no refund for quotas when refund account is empty
+        assert_eq!(
+            Quota::quota(1),
+            Some(pallet_quota::Quota {
+                last_use: 5,
+                amount: 0
+            })
+        );
+    })
+}
+
+// TODO implement a mock weight to test if refund queue processing actually stops when reached limit
diff --git a/pallets/quota/src/traits.rs b/pallets/quota/src/traits.rs
new file mode 100644
index 0000000000000000000000000000000000000000..fb2e377fd0fed6a6718703a0a3b893ad62e6b332
--- /dev/null
+++ b/pallets/quota/src/traits.rs
@@ -0,0 +1,27 @@
+// Copyright 2021 Axiom-Team
+//
+// This file is part of Duniter-v2S.
+//
+// Duniter-v2S is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, version 3 of the License.
+//
+// Duniter-v2S 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 Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with Duniter-v2S. If not, see <https://www.gnu.org/licenses/>.
+
+use crate::*;
+
+/// trait used to request refund of a fee
+pub trait RefundFee<T: Config> {
+    /// request refund for the account `account` using the quotas of identity `identity`
+    fn request_refund(account: T::AccountId, identity: IdtyId<T>, amount: BalanceOf<T>);
+}
+// dummy impl
+impl<T: Config> RefundFee<T> for () {
+    fn request_refund(_account: T::AccountId, _identity: IdtyId<T>, _amount: BalanceOf<T>) {}
+}
diff --git a/pallets/quota/src/weights.rs b/pallets/quota/src/weights.rs
new file mode 100644
index 0000000000000000000000000000000000000000..c6cadaf108c4792cc32172ed5b2633d369cac5a1
--- /dev/null
+++ b/pallets/quota/src/weights.rs
@@ -0,0 +1,25 @@
+// tmp
+
+use frame_support::weights::Weight;
+
+pub trait WeightInfo {
+    fn queue_refund() -> Weight;
+    fn spend_quota() -> Weight;
+    fn try_refund() -> Weight;
+    fn do_refund() -> Weight;
+}
+
+impl WeightInfo for () {
+    fn queue_refund() -> Weight {
+        Weight::from_parts(999u64, 0)
+    }
+    fn spend_quota() -> Weight {
+        Weight::from_parts(999u64, 0)
+    }
+    fn try_refund() -> Weight {
+        Weight::from_parts(999u64, 0)
+    }
+    fn do_refund() -> Weight {
+        Weight::from_parts(999u64, 0)
+    }
+}
diff --git a/resources/metadata.scale b/resources/metadata.scale
index fe56431a1e9d65f5bf7b826f845fb741cae101f6..2680c3b02a4ca74e19ed66cc13eb88883db14aef 100644
Binary files a/resources/metadata.scale and b/resources/metadata.scale differ
diff --git a/runtime/common/Cargo.toml b/runtime/common/Cargo.toml
index daf069abe05154ac965a162a1537662445b1a9ac..620eb3c8c23fbc1ebbc97cec505f161934325897 100644
--- a/runtime/common/Cargo.toml
+++ b/runtime/common/Cargo.toml
@@ -40,6 +40,7 @@ std = [
     'pallet-certification/std',
     'pallet-distance/std',
     'pallet-duniter-account/std',
+    'pallet-quota/std',
     'pallet-duniter-wot/std',
     'pallet-grandpa/std',
     'pallet-identity/std',
@@ -75,6 +76,7 @@ pallet-authority-members = { path = '../../pallets/authority-members', default-f
 pallet-certification = { path = '../../pallets/certification', default-features = false }
 pallet-distance = { path = "../../pallets/distance", default-features = false }
 pallet-duniter-account = { path = '../../pallets/duniter-account', default-features = false }
+pallet-quota = { path = '../../pallets/quota', default-features = false }
 pallet-duniter-wot = { path = '../../pallets/duniter-wot', default-features = false }
 pallet-identity = { path = '../../pallets/identity', default-features = false }
 pallet-membership = { path = '../../pallets/membership', default-features = false }
diff --git a/runtime/common/src/entities.rs b/runtime/common/src/entities.rs
index 08e84207968fd181235faf5efd6cccb92970c694..823ae2a71c74fe098e3334998c2065c39f4d7770 100644
--- a/runtime/common/src/entities.rs
+++ b/runtime/common/src/entities.rs
@@ -75,6 +75,7 @@ macro_rules! declare_session_keys {
 #[cfg_attr(feature = "std", derive(Deserialize, Serialize))]
 #[derive(Clone, Encode, Decode, Default, Eq, PartialEq, RuntimeDebug, TypeInfo, MaxEncodedLen)]
 pub struct IdtyData {
+    /// number of the first claimable UD
     pub first_eligible_ud: pallet_universal_dividend::FirstEligibleUd,
 }
 
@@ -93,35 +94,6 @@ impl From<IdtyData> for pallet_universal_dividend::FirstEligibleUd {
     }
 }
 
-pub struct NewOwnerKeySigner(sp_core::sr25519::Public);
-
-impl sp_runtime::traits::IdentifyAccount for NewOwnerKeySigner {
-    type AccountId = crate::AccountId;
-    fn into_account(self) -> crate::AccountId {
-        <[u8; 32]>::from(self.0).into()
-    }
-}
-
-#[derive(Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, TypeInfo)]
-pub struct NewOwnerKeySignature(sp_core::sr25519::Signature);
-
-impl sp_runtime::traits::Verify for NewOwnerKeySignature {
-    type Signer = NewOwnerKeySigner;
-    fn verify<L: sp_runtime::traits::Lazy<[u8]>>(&self, msg: L, signer: &crate::AccountId) -> bool {
-        use sp_core::crypto::ByteArray as _;
-        match sp_core::sr25519::Public::from_slice(signer.as_ref()) {
-            Ok(signer) => self.0.verify(msg, &signer),
-            Err(()) => false,
-        }
-    }
-}
-
-impl From<sp_core::sr25519::Signature> for NewOwnerKeySignature {
-    fn from(a: sp_core::sr25519::Signature) -> Self {
-        NewOwnerKeySignature(a)
-    }
-}
-
 #[cfg_attr(feature = "std", derive(Deserialize, Serialize))]
 #[derive(Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, TypeInfo)]
 pub struct SmithMembershipMetaData<SessionKeysWrapper> {
diff --git a/runtime/common/src/pallets_config.rs b/runtime/common/src/pallets_config.rs
index e5aaa8c3ff5d82da3d25b61f03e694f0422ec58b..dc585e3f1e677c46fb8864e8423d66c96bb9b250 100644
--- a/runtime/common/src/pallets_config.rs
+++ b/runtime/common/src/pallets_config.rs
@@ -67,7 +67,7 @@ macro_rules! pallets_config {
             /// What to do if an account is fully reaped from the system.
             type OnKilledAccount = ();
             /// The data to be stored in an account.
-            type AccountData = pallet_duniter_account::AccountData<Balance>;
+            type AccountData = pallet_duniter_account::AccountData<Balance, IdtyIndex>;
             /// Weight information for the extrinsics of this pallet.
             type SystemWeightInfo = common_runtime::weights::frame_system::WeightInfo<Runtime>;
             /// This is used as an identifier of the chain. 42 is the generic substrate prefix.
@@ -101,11 +101,36 @@ macro_rules! pallets_config {
         // ACCOUNT //
 
         impl pallet_duniter_account::Config for Runtime {
+            type RuntimeEvent = RuntimeEvent;
             type AccountIdToSalt = sp_runtime::traits::ConvertInto;
             type MaxNewAccountsPerBlock = frame_support::pallet_prelude::ConstU32<1>;
             type NewAccountPrice = frame_support::traits::ConstU64<300>;
-            type RuntimeEvent = RuntimeEvent;
             type WeightInfo = common_runtime::weights::pallet_duniter_account::WeightInfo<Runtime>;
+            // does currency adapter in any case, but adds "refund with quota" feature
+            type InnerOnChargeTransaction = CurrencyAdapter<Balances, HandleFees>;
+            type Refund = Quota;
+        }
+
+        // QUOTA //
+        pub struct TreasuryAccountId;
+        impl frame_support::pallet_prelude::Get<AccountId> for TreasuryAccountId {
+            fn get() -> AccountId {
+                // TODO optimize: make this a constant
+                // calling Treasury.account_id() actually requires computation
+                Treasury::account_id()
+            }
+        }
+        parameter_types! {
+            pub const ReloadRate: BlockNumber = 1 * HOURS; // faster than DAYS
+            pub const MaxQuota: Balance = 1000; // 10 ÄžD
+        }
+        impl pallet_quota::Config for Runtime {
+            type RuntimeEvent = RuntimeEvent;
+            // type IdtyId = IdtyIndex;
+            type ReloadRate = ReloadRate;
+            type MaxQuota = MaxQuota;
+            type RefundAccount = TreasuryAccountId;
+            type WeightInfo = common_runtime::weights::pallet_quota::WeightInfo<Runtime>;
         }
 
         // BLOCK CREATION //
@@ -113,22 +138,16 @@ macro_rules! pallets_config {
         impl pallet_babe::Config for Runtime {
             type EpochDuration = EpochDuration;
             type ExpectedBlockTime = ExpectedBlockTime;
-
             // session module is the trigger
             type EpochChangeTrigger = pallet_babe::ExternalTrigger;
-
             type DisabledValidators = Session;
-
             type KeyOwnerProof = <Historical as KeyOwnerProofSystem<(
                 KeyTypeId,
                 pallet_babe::AuthorityId,
             )>>::Proof;
-
             type EquivocationReportSystem =
                 pallet_babe::EquivocationReportSystem<Self, Offences, Historical, ReportLongevity>;
-
             type WeightInfo = common_runtime::weights::pallet_babe::WeightInfo<Runtime>;
-
             type MaxAuthorities = MaxAuthorities;
         }
 
@@ -142,6 +161,7 @@ macro_rules! pallets_config {
         // MONEY MANAGEMENT //
 
         impl pallet_balances::Config for Runtime {
+            type RuntimeEvent = RuntimeEvent;
             type MaxLocks = MaxLocks;
             type MaxReserves = frame_support::pallet_prelude::ConstU32<5>;
             type ReserveIdentifier = [u8; 8];
@@ -154,8 +174,6 @@ macro_rules! pallets_config {
 			type FreezeIdentifier = ();
 			type MaxHolds = frame_support::pallet_prelude::ConstU32<0>;
 			type MaxFreezes = frame_support::pallet_prelude::ConstU32<0>;
-            /// The ubiquitous event type.
-            type RuntimeEvent = RuntimeEvent;
             type WeightInfo = common_runtime::weights::pallet_balances::WeightInfo<Runtime>;
         }
 
@@ -171,19 +189,29 @@ macro_rules! pallets_config {
             }
         }
 
+        // fees are moved to the treasury
         pub struct HandleFees;
         type NegativeImbalance = <Balances as frame_support::traits::Currency<AccountId>>::NegativeImbalance;
         impl frame_support::traits::OnUnbalanced<NegativeImbalance> for HandleFees {
             fn on_nonzero_unbalanced(amount: NegativeImbalance) {
                 use frame_support::traits::Currency as _;
 
-                if let Some(author) = Authorship::author() {
-                    Balances::resolve_creating(&author, amount);
-                }
+                // fee is moved to treasury
+                Balances::resolve_creating(&Treasury::account_id(), amount);
+                // should move the tip to author
+                // if let Some(author) = Authorship::author() {
+                //     Balances::resolve_creating(&author, amount);
+                // }
             }
         }
         pub struct OnChargeTransaction;
+
+        parameter_types! {
+            pub FeeMultiplier: pallet_transaction_payment::Multiplier = pallet_transaction_payment::Multiplier::one();
+        }
         impl pallet_transaction_payment::Config for Runtime {
+            type RuntimeEvent = RuntimeEvent;
+            // does a filter on the call
             type OnChargeTransaction = OneshotAccount;
             type OperationalFeeMultiplier = frame_support::traits::ConstU8<5>;
             #[cfg(not(feature = "runtime-benchmarks"))]
@@ -191,13 +219,13 @@ macro_rules! pallets_config {
             #[cfg(feature = "runtime-benchmarks")]
             type WeightToFee = frame_support::weights::ConstantMultiplier::<u64, sp_core::ConstU64<0u64>>;
             type LengthToFee = common_runtime::fees::LengthToFeeImpl<Balance>;
-            type FeeMultiplierUpdate = ();
-            type RuntimeEvent = RuntimeEvent;
+            type FeeMultiplierUpdate = pallet_transaction_payment::ConstFeeMultiplier<FeeMultiplier>;
         }
         impl pallet_oneshot_account::Config for Runtime {
-            type Currency = Balances;
             type RuntimeEvent = RuntimeEvent;
-            type InnerOnChargeTransaction = CurrencyAdapter<Balances, HandleFees>;
+            type Currency = Balances;
+            // when call is not oneshot account, fall back to duniter-account implementation
+            type InnerOnChargeTransaction = Account;
             type WeightInfo = common_runtime::weights::pallet_oneshot_account::WeightInfo<Runtime>;
         }
 
@@ -207,6 +235,7 @@ macro_rules! pallets_config {
             type MaxAuthorities = MaxAuthorities;
         }
         impl pallet_authority_members::Config for Runtime {
+            type RuntimeEvent = RuntimeEvent;
             type KeysWrapper = opaque::SessionKeysWrapper;
             type IsMember = SmithMembership;
             type OnNewSession = OnNewSessionHandler<Runtime>;
@@ -215,7 +244,6 @@ macro_rules! pallets_config {
             type MemberIdOf = common_runtime::providers::IdentityIndexOf<Self>;
             type MaxAuthorities = MaxAuthorities;
             type RemoveMemberOrigin = EnsureRoot<Self::AccountId>;
-            type RuntimeEvent = RuntimeEvent;
 			type WeightInfo = common_runtime::weights::pallet_authority_members::WeightInfo<Runtime>;
         }
         impl pallet_authorship::Config for Runtime {
@@ -223,8 +251,8 @@ macro_rules! pallets_config {
             type EventHandler = ImOnline;
         }
         impl pallet_im_online::Config for Runtime {
-            type AuthorityId = ImOnlineId;
             type RuntimeEvent = RuntimeEvent;
+            type AuthorityId = ImOnlineId;
             type ValidatorSet = Historical;
             type NextSessionRotation = Babe;
             type ReportUnresponsiveness = Offences;
@@ -259,21 +287,18 @@ macro_rules! pallets_config {
         }
         impl pallet_grandpa::Config for Runtime {
             type RuntimeEvent = RuntimeEvent;
-
             type KeyOwnerProof =
                 <Historical as KeyOwnerProofSystem<(KeyTypeId, GrandpaId)>>::Proof;
-
             type EquivocationReportSystem =
             		pallet_grandpa::EquivocationReportSystem<Self, Offences, Historical, ReportLongevity>;
-
             type WeightInfo = common_runtime::weights::pallet_grandpa::WeightInfo<Runtime>;
-
             type MaxAuthorities = MaxAuthorities;
 			type MaxSetIdSessionEntries = MaxSetIdSessionEntries;
 		}
-parameter_types! {
-	pub const MaxSetIdSessionEntries: u32 = 1000;//BondingDuration::get() * SessionsPerEra::get();
-}
+        parameter_types! {
+            // BondingDuration::get() * SessionsPerEra::get();
+            pub const MaxSetIdSessionEntries: u32 = 1000;
+        }
 
         // ONCHAIN GOVERNANCE //
 
@@ -301,8 +326,8 @@ parameter_types! {
         }
 
         impl pallet_preimage::Config for Runtime {
-            type WeightInfo = common_runtime::weights::pallet_preimage::WeightInfo<Runtime>;
             type RuntimeEvent = RuntimeEvent;
+            type WeightInfo = common_runtime::weights::pallet_preimage::WeightInfo<Runtime>;
             type Currency = Balances;
             type ManagerOrigin = EnsureRoot<AccountId>;
             type BaseDeposit = PreimageBaseDeposit;
@@ -318,6 +343,7 @@ parameter_types! {
         }
 
         impl pallet_provide_randomness::Config for Runtime {
+            type RuntimeEvent = RuntimeEvent;
             type Currency = Balances;
             type GetCurrentEpochIndex = GetCurrentEpochIndex<Self>;
             type MaxRequests = frame_support::traits::ConstU32<100>;
@@ -326,7 +352,6 @@ parameter_types! {
             type OnUnbalanced = Treasury;
             type ParentBlockRandomness = pallet_babe::ParentBlockRandomness<Self>;
             type RandomnessFromOneEpochAgo = pallet_babe::RandomnessFromOneEpochAgo<Self>;
-            type RuntimeEvent = RuntimeEvent;
 			type WeightInfo = common_runtime::weights::pallet_provide_randomness::WeightInfo<Runtime>;
         }
 
@@ -413,7 +438,7 @@ parameter_types! {
 
         impl pallet_universal_dividend::Config for Runtime {
             type MomentIntoBalance = sp_runtime::traits::ConvertInto;
-            type Currency = pallet_balances::Pallet<Runtime>;
+            type Currency = Balances;
             type RuntimeEvent = RuntimeEvent;
 			type MaxPastReeval = frame_support::traits::ConstU32<160>;
             type MembersCount = MembersCount;
@@ -444,14 +469,13 @@ parameter_types! {
             type IdtyCreationPeriod = IdtyCreationPeriod;
 			type IdtyData = IdtyData;
             type IdtyIndex = IdtyIndex;
+            type AccountLinker = Account;
             type IdtyNameValidator = IdtyNameValidatorImpl;
             type IdtyRemovalOtherReason = pallet_duniter_wot::IdtyRemovalWotReason;
-            type NewOwnerKeySigner = <NewOwnerKeySignature as sp_runtime::traits::Verify>::Signer;
-			type NewOwnerKeySignature = NewOwnerKeySignature;
-            type OnIdtyChange = (common_runtime::handlers::OnIdtyChangeHandler<Runtime>, Wot);
+            type Signer = <Signature as sp_runtime::traits::Verify>::Signer;
+			type Signature = Signature;
+            type OnIdtyChange = (common_runtime::handlers::OnIdtyChangeHandler<Runtime>, Wot, Quota, Account);
             type RemoveIdentityConsumers = RemoveIdentityConsumersImpl<Self>;
-            type RevocationSigner = <Signature as sp_runtime::traits::Verify>::Signer;
-            type RevocationSignature = Signature;
             type RuntimeEvent = RuntimeEvent;
             type WeightInfo = common_runtime::weights::pallet_identity::WeightInfo<Runtime>;
             #[cfg(feature = "runtime-benchmarks")]
@@ -549,7 +573,7 @@ parameter_types! {
         }
         parameter_types! {
             pub const TechnicalCommitteeMotionDuration: BlockNumber = 7 * DAYS;
-pub MaxProposalWeight: Weight = Perbill::from_percent(50) * BlockWeights::get().max_block;
+            pub MaxProposalWeight: Weight = Perbill::from_percent(50) * BlockWeights::get().max_block;
         }
         impl pallet_collective::Config<Instance2> for Runtime {
             type RuntimeOrigin = RuntimeOrigin;
@@ -559,8 +583,8 @@ pub MaxProposalWeight: Weight = Perbill::from_percent(50) * BlockWeights::get().
             type MaxProposals = frame_support::pallet_prelude::ConstU32<20>;
             type MaxMembers = frame_support::pallet_prelude::ConstU32<100>;
             type WeightInfo = common_runtime::weights::pallet_collective::WeightInfo<Runtime>;
-type SetMembersOrigin = EnsureRoot<AccountId>;
-type MaxProposalWeight = MaxProposalWeight;
+            type SetMembersOrigin = EnsureRoot<AccountId>;
+            type MaxProposalWeight = MaxProposalWeight;
             #[cfg(not(feature = "runtime-benchmarks"))]
             type DefaultVote = TechnicalCommitteeDefaultVote;
             #[cfg(feature = "runtime-benchmarks")]
diff --git a/runtime/common/src/weights.rs b/runtime/common/src/weights.rs
index 952423d90b95fdfacfd4f13e4faa6432b0b1e67c..f8ab1253ae5c7bf8f9d06f4860b40cccb1c863ae 100644
--- a/runtime/common/src/weights.rs
+++ b/runtime/common/src/weights.rs
@@ -40,6 +40,7 @@ pub mod pallet_identity;
 pub mod pallet_preimage;
 pub mod pallet_utility;
 pub mod pallet_duniter_account;
+pub mod pallet_quota;
 pub mod pallet_oneshot_account;
 pub mod pallet_certification_cert;
 pub mod pallet_certification_smith_cert;
diff --git a/runtime/common/src/weights/pallet_duniter_account.rs b/runtime/common/src/weights/pallet_duniter_account.rs
index 62e4d4e49edd381d1e360ac98f11b030be87c451..675cf9303890bea6d4d661e407b2af1bfddb7e09 100644
--- a/runtime/common/src/weights/pallet_duniter_account.rs
+++ b/runtime/common/src/weights/pallet_duniter_account.rs
@@ -48,6 +48,17 @@ use core::marker::PhantomData;
 /// Weight functions for `pallet_duniter_account`.
 pub struct WeightInfo<T>(PhantomData<T>);
 impl<T: frame_system::Config> pallet_duniter_account::WeightInfo for WeightInfo<T> {
+	/// Storage: System Account (r:1 w:0)
+	/// Proof: System Account (max_values: None, max_size: Some(126), added: 2601, mode: MaxEncodedLen)
+	fn unlink_identity() -> Weight {
+		// Proof Size summary in bytes:
+		//  Measured:  `0`
+		//  Estimated: `3591`
+		// Minimum execution time: 95_130_000 picoseconds.
+		Weight::from_parts(110_501_000, 0)
+			.saturating_add(Weight::from_parts(0, 3591))
+			.saturating_add(T::DbWeight::get().reads(1))
+	}
 	/// Storage: Account PendingNewAccounts (r:1 w:1)
 	/// Proof Skipped: Account PendingNewAccounts (max_values: None, max_size: None, mode: Measured)
 	/// Storage: ProvideRandomness RequestIdProvider (r:1 w:1)
diff --git a/runtime/common/src/weights/pallet_identity.rs b/runtime/common/src/weights/pallet_identity.rs
index 2d2e0eb35baaedb4237b9223f50b1249d27f9008..11cea0a9a07890d30928a836eb35522193e07877 100644
--- a/runtime/common/src/weights/pallet_identity.rs
+++ b/runtime/common/src/weights/pallet_identity.rs
@@ -236,4 +236,20 @@ impl<T: frame_system::Config> pallet_identity::WeightInfo for WeightInfo<T> {
 			.saturating_add(T::DbWeight::get().reads(1))
 			.saturating_add(T::DbWeight::get().writes(1))
 	}
+	/// Storage: Identity IdentityIndexOf (r:1 w:0)
+	/// Proof Skipped: Identity IdentityIndexOf (max_values: None, max_size: None, mode: Measured)
+	/// Storage: System BlockHash (r:1 w:0)
+	/// Proof: System BlockHash (max_values: None, max_size: Some(44), added: 2519, mode: MaxEncodedLen)
+	/// Storage: System Account (r:1 w:1)
+	/// Proof: System Account (max_values: None, max_size: Some(126), added: 2601, mode: MaxEncodedLen)
+	fn link_account() -> Weight {
+		// Proof Size summary in bytes:
+		//  Measured:  `359`
+		//  Estimated: `3824`
+		// Minimum execution time: 543_046_000 picoseconds.
+		Weight::from_parts(544_513_000, 0)
+			.saturating_add(Weight::from_parts(0, 3824))
+			.saturating_add(T::DbWeight::get().reads(3))
+			.saturating_add(T::DbWeight::get().writes(1))
+	}
 }
diff --git a/runtime/common/src/weights/pallet_quota.rs b/runtime/common/src/weights/pallet_quota.rs
new file mode 100644
index 0000000000000000000000000000000000000000..eed4881948252d99b293b4132ac3c28221040ba7
--- /dev/null
+++ b/runtime/common/src/weights/pallet_quota.rs
@@ -0,0 +1,95 @@
+// Copyright 2021-2022 Axiom-Team
+//
+// This file is part of Duniter-v2S.
+//
+// Duniter-v2S is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, version 3 of the License.
+//
+// Duniter-v2S 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 Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with Duniter-v2S. If not, see <https://www.gnu.org/licenses/>.
+
+//! Autogenerated weights for `pallet_quota`
+//!
+//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev
+//! DATE: 2023-10-26, STEPS: `5`, REPEAT: `2`, LOW RANGE: `[]`, HIGH RANGE: `[]`
+//! WORST CASE MAP SIZE: `1000000`
+//! HOSTNAME: `Albatros`, CPU: `Intel(R) Core(TM) i7-8565U CPU @ 1.80GHz`
+//! EXECUTION: Some(Wasm), WASM-EXECUTION: Compiled, CHAIN: Some("gdev-benchmark"), DB CACHE: 1024
+
+// Executed Command:
+// ./target/debug/duniter
+// benchmark
+// pallet
+// --chain=gdev-benchmark
+// --steps=5
+// --repeat=2
+// --pallet=pallet_quota
+// --extrinsic=*
+// --execution=wasm
+// --wasm-execution=compiled
+// --heap-pages=4096
+// --output=./
+// --header=./file_header.txt
+
+#![cfg_attr(rustfmt, rustfmt_skip)]
+#![allow(unused_parens)]
+#![allow(unused_imports)]
+#![allow(missing_docs)]
+
+use frame_support::{traits::Get, weights::Weight};
+use core::marker::PhantomData;
+
+/// Weight functions for `pallet_quota`.
+pub struct WeightInfo<T>(PhantomData<T>);
+impl<T: frame_system::Config> pallet_quota::WeightInfo for WeightInfo<T> {
+	/// Storage: Quota RefundQueue (r:1 w:1)
+	/// Proof: Quota RefundQueue (max_values: Some(1), max_size: Some(11266), added: 11761, mode: MaxEncodedLen)
+	fn queue_refund() -> Weight {
+		// Proof Size summary in bytes:
+		//  Measured:  `42`
+		//  Estimated: `12751`
+		// Minimum execution time: 73_265_000 picoseconds.
+		Weight::from_parts(77_698_000, 0)
+			.saturating_add(Weight::from_parts(0, 12751))
+			.saturating_add(T::DbWeight::get().reads(1))
+			.saturating_add(T::DbWeight::get().writes(1))
+	}
+	/// Storage: Quota IdtyQuota (r:1 w:1)
+	/// Proof: Quota IdtyQuota (max_values: None, max_size: Some(24), added: 2499, mode: MaxEncodedLen)
+	fn spend_quota() -> Weight {
+		// Proof Size summary in bytes:
+		//  Measured:  `137`
+		//  Estimated: `3489`
+		// Minimum execution time: 147_746_000 picoseconds.
+		Weight::from_parts(165_850_000, 0)
+			.saturating_add(Weight::from_parts(0, 3489))
+			.saturating_add(T::DbWeight::get().reads(1))
+			.saturating_add(T::DbWeight::get().writes(1))
+	}
+	/// Storage: Quota IdtyQuota (r:1 w:1)
+	/// Proof: Quota IdtyQuota (max_values: None, max_size: Some(24), added: 2499, mode: MaxEncodedLen)
+	fn try_refund() -> Weight {
+		// Proof Size summary in bytes:
+		//  Measured:  `137`
+		//  Estimated: `3489`
+		// Minimum execution time: 367_239_000 picoseconds.
+		Weight::from_parts(392_186_000, 0)
+			.saturating_add(Weight::from_parts(0, 3489))
+			.saturating_add(T::DbWeight::get().reads(1))
+			.saturating_add(T::DbWeight::get().writes(1))
+	}
+	fn do_refund() -> Weight {
+		// Proof Size summary in bytes:
+		//  Measured:  `0`
+		//  Estimated: `0`
+		// Minimum execution time: 356_707_000 picoseconds.
+		Weight::from_parts(471_930_000, 0)
+			.saturating_add(Weight::from_parts(0, 0))
+	}
+}
diff --git a/runtime/g1/Cargo.toml b/runtime/g1/Cargo.toml
index 13433cb5822ef2145bac48061f2ce4965ee926c8..b25a268daea262dc34441fb80e37120a088cb125 100644
--- a/runtime/g1/Cargo.toml
+++ b/runtime/g1/Cargo.toml
@@ -46,6 +46,7 @@ std = [
     'pallet-distance/std',
     'pallet-duniter-test-parameters/std',
     'pallet-duniter-account/std',
+    'pallet-quota/std',
     'pallet-duniter-wot/std',
     'pallet-grandpa/std',
     'pallet-identity/std',
@@ -115,6 +116,7 @@ pallet-certification = { path = '../../pallets/certification', default-features
 pallet-distance = { path = "../../pallets/distance", default-features = false }
 pallet-duniter-test-parameters = { path = '../../pallets/duniter-test-parameters', default-features = false }
 pallet-duniter-account = { path = '../../pallets/duniter-account', default-features = false }
+pallet-quota = { path = '../../pallets/quota', default-features = false }
 pallet-duniter-wot = { path = '../../pallets/duniter-wot', default-features = false }
 pallet-identity = { path = '../../pallets/identity', default-features = false }
 pallet-membership = { path = '../../pallets/membership', default-features = false }
diff --git a/runtime/g1/src/lib.rs b/runtime/g1/src/lib.rs
index 57681a89409ff30ced0208f9d27435758dba458b..920d06390f3a711b52ab65f1cc967d56b92406c4 100644
--- a/runtime/g1/src/lib.rs
+++ b/runtime/g1/src/lib.rs
@@ -48,7 +48,9 @@ use pallet_grandpa::fg_primitives;
 use pallet_grandpa::{AuthorityId as GrandpaId, AuthorityList as GrandpaAuthorityList};
 use sp_api::impl_runtime_apis;
 use sp_core::OpaqueMetadata;
-use sp_runtime::traits::{AccountIdLookup, BlakeTwo256, Block as BlockT, NumberFor, OpaqueKeys};
+use sp_runtime::traits::{
+    AccountIdLookup, BlakeTwo256, Block as BlockT, NumberFor, One, OpaqueKeys,
+};
 use sp_runtime::{
     create_runtime_str, generic, impl_opaque_keys,
     transaction_validity::{TransactionSource, TransactionValidity},
@@ -253,6 +255,7 @@ construct_runtime!(
         Balances: pallet_balances::{Pallet, Call, Storage, Config<T>, Event<T>} = 6,
         TransactionPayment: pallet_transaction_payment::{Pallet, Storage, Event<T>} = 32,
         OneshotAccount: pallet_oneshot_account::{Pallet, Call, Storage, Event<T>} = 7,
+        Quota: pallet_quota::{Pallet, Storage, Config<T>, Event<T>} = 66,
 
         // Consensus support.
         AuthorityMembers: pallet_authority_members::{Pallet, Call, Storage, Config<T>, Event<T>} = 10,
diff --git a/runtime/gdev/Cargo.toml b/runtime/gdev/Cargo.toml
index c7a84e4994097901c544a920207e75ab31806e3f..93f07bc788f15f6c8e8eb6d6e8aaa54ab473e104 100644
--- a/runtime/gdev/Cargo.toml
+++ b/runtime/gdev/Cargo.toml
@@ -29,6 +29,7 @@ runtime-benchmarks = [
     'pallet-collective/runtime-benchmarks',
     'pallet-duniter-test-parameters/runtime-benchmarks',
     'pallet-duniter-account/runtime-benchmarks',
+    'pallet-quota/runtime-benchmarks',
     'pallet-duniter-wot/runtime-benchmarks',
     'pallet-grandpa/runtime-benchmarks',
     'pallet-identity/runtime-benchmarks',
@@ -67,6 +68,7 @@ std = [
     'pallet-distance/std',
     'pallet-duniter-test-parameters/std',
     'pallet-duniter-account/std',
+    'pallet-quota/std',
     'pallet-duniter-wot/std',
     'pallet-grandpa/std',
     'pallet-identity/std',
@@ -141,6 +143,7 @@ pallet-certification = { path = '../../pallets/certification', default-features
 pallet-distance = { path = "../../pallets/distance", default-features = false }
 pallet-duniter-test-parameters = { path = '../../pallets/duniter-test-parameters', default-features = false }
 pallet-duniter-account = { path = '../../pallets/duniter-account', default-features = false }
+pallet-quota = { path = '../../pallets/quota', default-features = false }
 pallet-duniter-wot = { path = '../../pallets/duniter-wot', default-features = false }
 pallet-identity = { path = '../../pallets/identity', default-features = false }
 pallet-membership = { path = '../../pallets/membership', default-features = false }
diff --git a/runtime/gdev/src/lib.rs b/runtime/gdev/src/lib.rs
index fd22c7bf6cd163e6a3aa3abffe3f448bc1c9e916..15abf4260691588a8eef9c0d9725869dadfda0a3 100644
--- a/runtime/gdev/src/lib.rs
+++ b/runtime/gdev/src/lib.rs
@@ -55,7 +55,9 @@ use pallet_grandpa::fg_primitives;
 use pallet_grandpa::{AuthorityId as GrandpaId, AuthorityList as GrandpaAuthorityList};
 use sp_api::impl_runtime_apis;
 use sp_core::OpaqueMetadata;
-use sp_runtime::traits::{AccountIdLookup, BlakeTwo256, Block as BlockT, NumberFor, OpaqueKeys};
+use sp_runtime::traits::{
+    AccountIdLookup, BlakeTwo256, Block as BlockT, NumberFor, One, OpaqueKeys,
+};
 use sp_runtime::{
     create_runtime_str, generic, impl_opaque_keys,
     transaction_validity::{TransactionSource, TransactionValidity},
@@ -148,6 +150,7 @@ mod benches {
         [pallet_provide_randomness, ProvideRandomness]
         [pallet_upgrade_origin, UpgradeOrigin]
         [pallet_duniter_account, Account]
+        [pallet_quota, Quota]
         [pallet_identity, Identity]
         [pallet_membership, Membership]
         [pallet_membership, SmithMembership]
@@ -296,7 +299,7 @@ construct_runtime!(
     {
         // Basic stuff
         System: frame_system::{Pallet, Call, Config, Storage, Event<T>} = 0,
-        Account: pallet_duniter_account::{Pallet, Storage, Config<T>, Event<T>} = 1,
+        Account: pallet_duniter_account::{Pallet, Call, Storage, Config<T>, Event<T>} = 1,
         Scheduler: pallet_scheduler::{Pallet, Call, Storage, Event<T>} = 2,
 
         // Block creation
@@ -310,6 +313,7 @@ construct_runtime!(
         Balances: pallet_balances::{Pallet, Call, Storage, Config<T>, Event<T>} = 6,
         TransactionPayment: pallet_transaction_payment::{Pallet, Storage, Event<T>} = 32,
         OneshotAccount: pallet_oneshot_account::{Pallet, Call, Storage, Event<T>} = 7,
+        Quota: pallet_quota::{Pallet, Storage, Config<T>, Event<T>} = 66,
 
         // Consensus support
         AuthorityMembers: pallet_authority_members::{Pallet, Call, Storage, Config<T>, Event<T>} = 10,
diff --git a/runtime/gdev/tests/common/mod.rs b/runtime/gdev/tests/common/mod.rs
index 8561f1655739e8ffbe399eb958ed2642c94f493e..66b0a2da0cf6d37fca92fd8f199ab103af3e9d16 100644
--- a/runtime/gdev/tests/common/mod.rs
+++ b/runtime/gdev/tests/common/mod.rs
@@ -50,7 +50,7 @@ pub const NAMES: [&str; 6] = ["Alice", "Bob", "Charlie", "Dave", "Eve", "Ferdie"
 
 pub struct ExtBuilder {
     // endowed accounts with balances
-    initial_accounts: BTreeMap<AccountId, GenesisAccountData<Balance>>,
+    initial_accounts: BTreeMap<AccountId, GenesisAccountData<Balance, u32>>,
     initial_authorities_len: usize,
     initial_identities: BTreeMap<IdtyName, AccountId>,
     initial_smiths: Vec<AuthorityKeys>,
@@ -73,8 +73,8 @@ impl ExtBuilder {
                     get_account_id_from_seed::<sr25519::Public>(NAMES[i]),
                     GenesisAccountData {
                         balance: 0,
-                        is_identity: true,
                         random_id: H256([i as u8; 32]),
+                        idty_id: Some(i as u32 + 1),
                     },
                 )
             })
@@ -231,6 +231,16 @@ impl ExtBuilder {
         .assimilate_storage(&mut t)
         .unwrap();
 
+        pallet_quota::GenesisConfig::<Runtime> {
+            identities: initial_identities
+                .iter()
+                .enumerate()
+                .map(|(i, _)| i as u32 + 1)
+                .collect(),
+        }
+        .assimilate_storage(&mut t)
+        .unwrap();
+
         pallet_membership::GenesisConfig::<Runtime, Instance1> {
             memberships: (1..=initial_identities.len())
                 .map(|i| {
diff --git a/runtime/gdev/tests/integration_tests.rs b/runtime/gdev/tests/integration_tests.rs
index 012e6a3b67df5ae8c7f19a3e55b307445f35c6f9..1690e0a49b08d200b72495abf7d834fb9733d88c 100644
--- a/runtime/gdev/tests/integration_tests.rs
+++ b/runtime/gdev/tests/integration_tests.rs
@@ -18,11 +18,13 @@ mod common;
 
 use common::*;
 use frame_support::instances::Instance1;
+use frame_support::traits::StoredMap;
 use frame_support::traits::{Get, PalletInfo, StorageInfo, StorageInfoTrait};
 use frame_support::{assert_noop, assert_ok};
 use frame_support::{StorageHasher, Twox128};
 use gdev_runtime::*;
 use pallet_duniter_wot::IdtyRemovalWotReason;
+use sp_core::Encode;
 use sp_keyring::AccountKeyring;
 use sp_runtime::MultiAddress;
 
@@ -704,6 +706,9 @@ fn test_create_new_account_with_insufficient_balance() {
         .build()
         .execute_with(|| {
             run_to_block(2);
+            // Treasury should start empty
+            // FIXME it actually starts with ED
+            assert_eq!(Balances::free_balance(Treasury::account_id()), 100);
 
             // Should be able to transfer 4 units to a new account
             assert_ok!(Balances::transfer(
@@ -747,7 +752,7 @@ fn test_create_new_account_with_insufficient_balance() {
                 Balances::free_balance(AccountKeyring::Eve.to_account_id()),
                 0
             );
-            // 100 initial + 300 recycled from Eve account's destructuion
+            // 100 initial + 300 recycled from Eve account's destruction
             assert_eq!(Balances::free_balance(Treasury::account_id()), 400);
         });
 }
@@ -760,6 +765,7 @@ fn test_create_new_account() {
         .build()
         .execute_with(|| {
             run_to_block(2);
+            assert_eq!(Balances::free_balance(Treasury::account_id()), 100);
 
             // Should be able to transfer 5 units to a new account
             assert_ok!(Balances::transfer(
@@ -1118,3 +1124,135 @@ fn test_oneshot_accounts() {
             );
         });
 }
+
+/// test currency transfer
+/// (does not take fees into account because it's only calls, not extrinsics)
+#[test]
+fn test_transfer() {
+    ExtBuilder::new(1, 3, 4)
+        .with_initial_balances(vec![
+            (AccountKeyring::Alice.to_account_id(), 10_000),
+            (AccountKeyring::Eve.to_account_id(), 10_000),
+        ])
+        .build()
+        .execute_with(|| {
+            // Alice gives 500 to Eve
+            assert_ok!(Balances::transfer_allow_death(
+                frame_system::RawOrigin::Signed(AccountKeyring::Alice.to_account_id()).into(),
+                AccountKeyring::Eve.to_account_id().into(),
+                500
+            ));
+            // check amounts
+            assert_eq!(
+                Balances::free_balance(AccountKeyring::Alice.to_account_id()),
+                10_000 - 500
+            );
+            assert_eq!(
+                Balances::free_balance(AccountKeyring::Eve.to_account_id()),
+                10_000 + 500
+            );
+        })
+}
+
+/// test linking account to identity
+#[test]
+fn test_link_account() {
+    ExtBuilder::new(1, 3, 4).build().execute_with(|| {
+        let genesis_hash = System::block_hash(0);
+        let alice = AccountKeyring::Alice.to_account_id();
+        let ferdie = AccountKeyring::Ferdie.to_account_id();
+        let payload = (b"link", genesis_hash, 1u32, ferdie.clone()).encode();
+        let signature = AccountKeyring::Ferdie.sign(&payload);
+        // Ferdie's account can be linked to Alice identity
+        assert_ok!(Identity::link_account(
+            frame_system::RawOrigin::Signed(alice).into(),
+            ferdie,
+            signature.into()
+        ));
+    })
+}
+
+/// test change owner key
+#[test]
+fn test_change_owner_key() {
+    ExtBuilder::new(1, 3, 4).build().execute_with(|| {
+        let genesis_hash = System::block_hash(0);
+        let dave = AccountKeyring::Dave.to_account_id();
+        let ferdie = AccountKeyring::Ferdie.to_account_id();
+        let payload = (b"icok", genesis_hash, 4u32, dave.clone()).encode();
+        let signature = AccountKeyring::Ferdie.sign(&payload);
+        // Dave can change his owner key to Ferdie's
+        assert_ok!(Identity::change_owner_key(
+            frame_system::RawOrigin::Signed(dave).into(),
+            ferdie,
+            signature.into()
+        ));
+    })
+}
+
+/// test genesis account of identity is linked to identity
+// (and account without identity is not linked)
+#[test]
+fn test_genesis_account_of_identity_linked() {
+    ExtBuilder::new(1, 3, 4)
+        .with_initial_balances(vec![(AccountKeyring::Eve.to_account_id(), 8888)])
+        .build()
+        .execute_with(|| {
+            // Alice account
+            let account_id = AccountKeyring::Alice.to_account_id();
+            // Alice identity index is 1
+            assert_eq!(Identity::identity_index_of(&account_id), Some(1));
+            // get account data
+            let account_data = frame_system::Pallet::<Runtime>::get(&account_id);
+            assert_eq!(account_data.linked_idty, Some(1));
+            // Eve is not member, her account has no linked identity
+            assert_eq!(
+                frame_system::Pallet::<Runtime>::get(&AccountKeyring::Eve.to_account_id())
+                    .linked_idty,
+                None
+            );
+        })
+}
+
+/// test unlink identity from account
+#[test]
+fn test_unlink_identity() {
+    ExtBuilder::new(1, 3, 4).build().execute_with(|| {
+        let alice_account = AccountKeyring::Alice.to_account_id();
+        // check that Alice is 1
+        assert_eq!(Identity::identity_index_of(&alice_account), Some(1));
+
+        // Alice can unlink her identity from her account
+        assert_ok!(Account::unlink_identity(
+            frame_system::RawOrigin::Signed(AccountKeyring::Alice.to_account_id()).into(),
+        ));
+
+        // Alice account has been unlinked
+        assert_eq!(
+            frame_system::Pallet::<Runtime>::get(&alice_account).linked_idty,
+            None
+        );
+    })
+}
+
+/// test that the account of a newly created identity is linked to the identity
+#[test]
+fn test_new_account_linked() {
+    ExtBuilder::new(1, 3, 4).build().execute_with(|| {
+        let eve_account = AccountKeyring::Eve.to_account_id();
+        assert_eq!(
+            frame_system::Pallet::<Runtime>::get(&eve_account).linked_idty,
+            None
+        );
+        // Alice creates identity for Eve
+        assert_ok!(Identity::create_identity(
+            frame_system::RawOrigin::Signed(AccountKeyring::Alice.to_account_id()).into(),
+            eve_account.clone(),
+        ));
+        // then eve account should be linked to her identity
+        assert_eq!(
+            frame_system::Pallet::<Runtime>::get(&eve_account).linked_idty,
+            Some(5)
+        );
+    })
+}
diff --git a/runtime/gdev/tests/xt_tests.rs b/runtime/gdev/tests/xt_tests.rs
new file mode 100644
index 0000000000000000000000000000000000000000..1663d6221475aaea797b388399d8f0d1364aeeca
--- /dev/null
+++ b/runtime/gdev/tests/xt_tests.rs
@@ -0,0 +1,192 @@
+// Copyright 2021 Axiom-Team
+//
+// This file is part of Duniter-v2S.
+//
+// Duniter-v2S is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, version 3 of the License.
+//
+// Duniter-v2S 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 Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with Duniter-v2S. If not, see <https://www.gnu.org/licenses/>.
+
+// these integration tests aim to test fees and extrinsic-related externalities
+
+mod common;
+
+use common::*;
+use frame_support::assert_ok;
+use frame_support::inherent::Extrinsic;
+use frame_support::traits::OnIdle;
+use gdev_runtime::*;
+use sp_core::Encode;
+use sp_core::Pair;
+use sp_keyring::AccountKeyring;
+use sp_runtime::generic::SignedPayload;
+
+/// get extrinsic for given call
+fn get_unchecked_extrinsic(
+    call: RuntimeCall,
+    era: u64,
+    block: u64,
+    signer: AccountKeyring,
+    tip: Balance,
+) -> UncheckedExtrinsic {
+    let extra: gdev_runtime::SignedExtra = (
+        frame_system::CheckNonZeroSender::<gdev_runtime::Runtime>::new(),
+        frame_system::CheckSpecVersion::<gdev_runtime::Runtime>::new(),
+        frame_system::CheckTxVersion::<gdev_runtime::Runtime>::new(),
+        frame_system::CheckGenesis::<gdev_runtime::Runtime>::new(),
+        frame_system::CheckMortality::<gdev_runtime::Runtime>::from(
+            sp_runtime::generic::Era::mortal(era, block),
+        ),
+        frame_system::CheckNonce::<gdev_runtime::Runtime>::from(0u32).into(),
+        frame_system::CheckWeight::<gdev_runtime::Runtime>::new(),
+        pallet_transaction_payment::ChargeTransactionPayment::<gdev_runtime::Runtime>::from(tip),
+    );
+    let payload = SignedPayload::new(call.clone(), extra.clone()).unwrap();
+    let origin = signer;
+    let sig = payload.using_encoded(|payload| origin.pair().sign(payload));
+
+    UncheckedExtrinsic::new(
+        call,
+        Some((origin.to_account_id().into(), sig.into(), extra)),
+    )
+    .unwrap()
+}
+
+/// test currency transfer with extrinsic
+// the signer account should pay fees and a tip
+// the treasury should get the fees
+#[test]
+fn test_transfer_xt() {
+    ExtBuilder::new(1, 3, 4)
+        .with_initial_balances(vec![
+            (AccountKeyring::Alice.to_account_id(), 10_000),
+            (AccountKeyring::Eve.to_account_id(), 10_000),
+        ])
+        .build()
+        .execute_with(|| {
+            let call = RuntimeCall::Balances(BalancesCall::transfer_allow_death {
+                dest: AccountKeyring::Eve.to_account_id().into(),
+                value: 500,
+            });
+
+            // 1 cÄžD of tip
+            let xt = get_unchecked_extrinsic(call, 4u64, 8u64, AccountKeyring::Alice, 1u64);
+            // let info = xt.get_dispatch_info();
+            // println!("dispatch info:\n\t {:?}\n", info);
+
+            assert_eq!(Balances::free_balance(Treasury::account_id()), 100);
+            // Alice gives 500 to Eve
+            assert_ok!(Executive::apply_extrinsic(xt));
+            // check amounts
+            assert_eq!(
+                Balances::free_balance(AccountKeyring::Alice.to_account_id()),
+                10_000 - 500 - 3 // initial - transfered - fees
+            );
+            assert_eq!(
+                Balances::free_balance(AccountKeyring::Eve.to_account_id()),
+                10_000 + 500 // initial + transfered
+            );
+            assert_eq!(Balances::free_balance(Treasury::account_id()), 100 + 3);
+        })
+}
+
+/// test that fees are added to the refund queue
+#[test]
+fn test_refund_queue() {
+    ExtBuilder::new(1, 3, 4)
+        .with_initial_balances(vec![
+            (AccountKeyring::Alice.to_account_id(), 10_000),
+            (AccountKeyring::Eve.to_account_id(), 10_000),
+        ])
+        .build()
+        .execute_with(|| {
+            let call = RuntimeCall::Balances(BalancesCall::transfer_allow_death {
+                dest: AccountKeyring::Eve.to_account_id().into(),
+                value: 500,
+            });
+
+            // 1 cÄžD of tip
+            let xt = get_unchecked_extrinsic(call, 4u64, 8u64, AccountKeyring::Alice, 1u64);
+            assert_ok!(Executive::apply_extrinsic(xt));
+
+            // check that refund was added to the queue
+            assert_eq!(
+                pallet_quota::RefundQueue::<Runtime>::get()
+                    .first()
+                    .expect("a refund should have been added to the queue"),
+                &pallet_quota::pallet::Refund {
+                    account: AccountKeyring::Alice.to_account_id(),
+                    identity: 1u32,
+                    amount: 2u64
+                }
+            );
+        })
+}
+
+/// test refund on_idle
+#[test]
+fn test_refund_on_idle() {
+    ExtBuilder::new(1, 3, 4)
+        .with_initial_balances(vec![
+            (AccountKeyring::Alice.to_account_id(), 10_000),
+            (AccountKeyring::Eve.to_account_id(), 10_000),
+        ])
+        .build()
+        .execute_with(|| {
+            let call = RuntimeCall::Balances(BalancesCall::transfer_allow_death {
+                dest: AccountKeyring::Eve.to_account_id().into(),
+                value: 500,
+            });
+
+            // 1 cÄžD of tip
+            let xt = get_unchecked_extrinsic(call, 4u64, 8u64, AccountKeyring::Alice, 1u64);
+            assert_ok!(Executive::apply_extrinsic(xt));
+
+            // call on_idle to activate refund
+            Quota::on_idle(System::block_number(), Weight::from(1_000_000_000));
+
+            // check that refund event existed
+            System::assert_has_event(RuntimeEvent::Quota(pallet_quota::Event::Refunded {
+                who: AccountKeyring::Alice.to_account_id(),
+                identity: 1u32,
+                amount: 1u64,
+            }));
+
+            // check that refund queue is empty
+            assert!(pallet_quota::RefundQueue::<Runtime>::get().is_empty());
+            assert_eq!(
+                Balances::free_balance(AccountKeyring::Alice.to_account_id()),
+                10_000 - 500 - 1 - 2 + 1 // initial - transfered - tip - fees + refunded fees
+            );
+        })
+}
+
+/// test no refund when no identity linked
+#[test]
+fn test_no_refund() {
+    ExtBuilder::new(1, 3, 4)
+        .with_initial_balances(vec![
+            (AccountKeyring::Alice.to_account_id(), 10_000),
+            (AccountKeyring::Eve.to_account_id(), 10_000),
+        ])
+        .build()
+        .execute_with(|| {
+            // Eve → Alice
+            let call = RuntimeCall::Balances(BalancesCall::transfer_allow_death {
+                dest: AccountKeyring::Alice.to_account_id().into(),
+                value: 500,
+            });
+            let xt = get_unchecked_extrinsic(call, 4u64, 8u64, AccountKeyring::Eve, 1u64);
+            assert_ok!(Executive::apply_extrinsic(xt));
+            // check that refund queue is empty
+            assert!(pallet_quota::RefundQueue::<Runtime>::get().is_empty());
+            assert_eq!(Balances::free_balance(Treasury::account_id()), 100 + 3);
+        })
+}
diff --git a/runtime/gtest/Cargo.toml b/runtime/gtest/Cargo.toml
index a76ac672d2e03e078f7af411898cf38a71989173..1fcac625272450171cee4ae1e31cb801bbcb3fc4 100644
--- a/runtime/gtest/Cargo.toml
+++ b/runtime/gtest/Cargo.toml
@@ -65,6 +65,7 @@ std = [
     'pallet-collective/std',
     'pallet-distance/std',
     'pallet-duniter-account/std',
+    'pallet-quota/std',
     'pallet-duniter-wot/std',
     'pallet-grandpa/std',
     'pallet-identity/std',
@@ -94,6 +95,7 @@ std = [
     'sp-block-builder/std',
     'sp-consensus-babe/std',
     'sp-core/std',
+    'sp-distance/std',
     'sp-inherents/std',
     'sp-offchain/std',
     'sp-membership/std',
@@ -137,16 +139,18 @@ pallet-authority-members = { path = '../../pallets/authority-members', default-f
 pallet-certification = { path = '../../pallets/certification', default-features = false }
 pallet-distance = { path = "../../pallets/distance", default-features = false }
 pallet-duniter-account = { path = '../../pallets/duniter-account', default-features = false }
+pallet-quota = { path = '../../pallets/quota', default-features = false }
 pallet-duniter-wot = { path = '../../pallets/duniter-wot', default-features = false }
 pallet-identity = { path = '../../pallets/identity', default-features = false }
 pallet-membership = { path = '../../pallets/membership', default-features = false }
+pallet-offences = { path = '../../pallets/offences', default-features = false }
 pallet-oneshot-account = { path = '../../pallets/oneshot-account', default-features = false }
 pallet-provide-randomness = { path = '../../pallets/provide-randomness', default-features = false }
 pallet-universal-dividend = { path = '../../pallets/universal-dividend', default-features = false }
 pallet-session-benchmarking = { path = '../../pallets/session-benchmarking', default-features = false }
 pallet-upgrade-origin = { path = '../../pallets/upgrade-origin', default-features = false }
+sp-distance = { path = '../../primitives/distance', default-features = false }
 sp-membership = { path = '../../primitives/membership', default-features = false }
-pallet-offences = { path = '../../pallets/offences', default-features = false }
 
 # crates.io
 codec = { package = "parity-scale-codec", version = "3.4.0", features = ["derive"], default-features = false }
diff --git a/runtime/gtest/src/lib.rs b/runtime/gtest/src/lib.rs
index 4d3b0e4a0d5abfe9da06233e1c2d8d986c3f2e99..185c6abb1cf467467ba5f8459ca884044c3a4fc2 100644
--- a/runtime/gtest/src/lib.rs
+++ b/runtime/gtest/src/lib.rs
@@ -52,7 +52,9 @@ use pallet_grandpa::fg_primitives;
 use pallet_grandpa::{AuthorityId as GrandpaId, AuthorityList as GrandpaAuthorityList};
 use sp_api::impl_runtime_apis;
 use sp_core::OpaqueMetadata;
-use sp_runtime::traits::{AccountIdLookup, BlakeTwo256, Block as BlockT, NumberFor, OpaqueKeys};
+use sp_runtime::traits::{
+    AccountIdLookup, BlakeTwo256, Block as BlockT, NumberFor, One, OpaqueKeys,
+};
 use sp_runtime::{
     create_runtime_str, generic, impl_opaque_keys,
     transaction_validity::{TransactionSource, TransactionValidity},
@@ -269,6 +271,7 @@ construct_runtime!(
         Balances: pallet_balances::{Pallet, Call, Storage, Config<T>, Event<T>} = 6,
         TransactionPayment: pallet_transaction_payment::{Pallet, Storage, Event<T>} = 32,
         OneshotAccount: pallet_oneshot_account::{Pallet, Call, Storage, Event<T>} = 7,
+        Quota: pallet_quota::{Pallet, Storage, Config<T>, Event<T>} = 66,
 
         // Consensus support
         AuthorityMembers: pallet_authority_members::{Pallet, Call, Storage, Config<T>, Event<T>} = 10,
diff --git a/xtask/README.md b/xtask/README.md
index 3f856c57016a1badafd368db3324cdd8945f3edc..f9ee2d8980e7a608e5545c22b9a82011085c6864 100644
--- a/xtask/README.md
+++ b/xtask/README.md
@@ -6,4 +6,21 @@ We choose [`xtask`](https://github.com/matklad/cargo-xtask/) to run Rust scripts
 cargo xtask # this will build the scripts and show the available commands
 ```
 
-These scripts mainly deal with runtime operations.
\ No newline at end of file
+These scripts mainly deal with runtime operations.
+
+## Doc
+
+```
+Usage: xtask <COMMAND>
+
+Commands:
+  build                Build duniter binary
+  gen-calls-doc        Generate calls documentation
+  inject-runtime-code  Inject runtime code in raw specs
+  release-runtime      Release a new runtime
+  test                 Execute unit tests and integration tests End2tests are skipped
+  help                 Print this message or the help of the given subcommand(s)
+
+Options:
+  -h, --help  Print help
+```
diff --git a/xtask/res/templates/runtime-calls-category.md b/xtask/res/templates/runtime-calls-category.md
index d65ca060e4b9e6416631fb0bfd1768e16fc8bd3c..efe447ef4a9afeecea54745b9053e767670ab888 100644
--- a/xtask/res/templates/runtime-calls-category.md
+++ b/xtask/res/templates/runtime-calls-category.md
@@ -20,7 +20,7 @@ There are **{{ calls_counter }}** {{ category_name }} calls from **{{ pallets |
 </details>
 
 {# replace markdown sytax in documentation breaking the final result #}
-{{ call.documentation | replace(from="# WARNING:", to="WARNING:") }}
+{{ call.documentation | replace(from="# WARNING:", to="WARNING:") | replace(from="## Complexity", to="**Complexity**") }}
 
 {% endfor -%}
 {% endfor -%}