diff --git a/README.md b/README.md
index 74aedf6d8c470e55f18bc1beef92ae31dcdbe99c..29fcd18dbe80b6eff3e19eb318abdb8fb7e29482 100644
--- a/README.md
+++ b/README.md
@@ -121,7 +121,7 @@ Silkaj is based on following Python modules:
 
 - [Click](https://click.palletsprojects.com/): Composable command line interface toolkit
 - [DuniterPy](https://git.duniter.org/clients/python/duniterpy/): Most complete client oriented Python library for Duniter/Ğ1 ecosystem
-- [Arrow](https://arrow.readthedocs.io/): Better dates & times for Python
+- [Pendulum](https://pendulum.eustace.io/): Datetimes made easy
 - [texttable](https://github.com/foutaise/texttable/): Creation of simple ASCII tables
 
 ### Names
diff --git a/docs/contributing/index.md b/docs/contributing/index.md
index 06c6221f1defb377fde5c529fb6b1003319b4ca7..6a3ed6e74fb502fe01957da175c4bdc4ac1b2c58 100644
--- a/docs/contributing/index.md
+++ b/docs/contributing/index.md
@@ -52,7 +52,7 @@ Please read their documentations on how to use them the best possible.
     - Feel free to contribute upstream to share the code with other Python programs
 - [Click](https://click.palletsprojects.com/#documentation)
     - [Rich-Click](https://github.com/ewels/rich-click)
-- [Arrow](https://arrow.readthedocs.io/)
+- [Pendulum](https://pendulum.eustace.io/docs/)
 - [texttable](https://github.com/foutaise/texttable/#documentation)
 
 ## Pre-commit hooks
diff --git a/pyproject.toml b/pyproject.toml
index 32804676e1db8a0756c9aa049edf131534495709..1846302548a0469fb4ede927620ca93ecc4076cc 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -42,7 +42,7 @@ deathreaper = [ "pydiscourse (>=1.7.0,<2.0)" ]
 duniterpy = "~1.2.1"
 rich-click = "^1.8.8"
 texttable = "^1.7.0"
-arrow = "^1.3.0"
+pendulum = "^3.1.0"
 
 [tool.poetry.group.test.dependencies]
 pytest = "^8.3.5"
diff --git a/silkaj/blockchain/blocks.py b/silkaj/blockchain/blocks.py
index d49e4fa9cd1d3b7f54977803c96d54330459fdce..47cacb21f5b85990e5584e54d492f422a15c728f 100644
--- a/silkaj/blockchain/blocks.py
+++ b/silkaj/blockchain/blocks.py
@@ -17,7 +17,7 @@ import time
 from operator import itemgetter
 from urllib.error import HTTPError
 
-import arrow
+import pendulum
 import rich_click as click
 from duniterpy.api import bma
 
@@ -50,10 +50,12 @@ def list_blocks(number: int, detailed: bool) -> None:
         issuer["pubkey"] = block["issuer"]
         if detailed or number <= 30:
             issuer["block"] = block["number"]
-            issuer["gentime"] = arrow.get(block["time"]).to("local").format(ALL)
-            issuer["mediantime"] = (
-                arrow.get(block["medianTime"]).to("local").format(ALL)
-            )
+            issuer["gentime"] = pendulum.from_timestamp(
+                block["time"], tz="local"
+            ).format(ALL)
+            issuer["mediantime"] = pendulum.from_timestamp(
+                block["medianTime"], tz="local"
+            ).format(ALL)
             issuer["hash"] = block["hash"][:10]
             issuer["powMin"] = block["powMin"]
         issuers_dict[issuer["pubkey"]] = issuer
diff --git a/silkaj/blockchain/difficulty.py b/silkaj/blockchain/difficulty.py
index 4735c9cec0c790ec4ce071cbf99b8db7b01a45bb..fbd2897330f11e3c2bacbc48eafe9981a1ca092d 100644
--- a/silkaj/blockchain/difficulty.py
+++ b/silkaj/blockchain/difficulty.py
@@ -16,8 +16,8 @@
 from operator import itemgetter
 from os import system
 
-import arrow
 import jsonschema
+import pendulum
 import rich_click as click
 from duniterpy.api import bma
 from duniterpy.api.client import WSConnection
@@ -54,7 +54,7 @@ def display_diffi(current: WSConnection, diffi: dict) -> None:
         d["Π diffi"] = compute_power(match_pattern(d["level"])[1])
         d["Σ diffi"] = d.pop("level")
     system("cls||clear")
-    block_gen = arrow.get(current["time"]).to("local").format(ALL)
+    block_gen = pendulum.from_timestamp(current["time"], tz="local").format(ALL)
     match = match_pattern(int(current["powMin"]))[0]
 
     table = tui.Table(style="columns").set_cols_dtype(["t", "t", "t", "i"])
diff --git a/silkaj/blockchain/information.py b/silkaj/blockchain/information.py
index d4d8f503ea4f5fcfb0851eb8a480184488f422a5..225433bba5cda498fc4d1e7f0e11097b6125a52c 100644
--- a/silkaj/blockchain/information.py
+++ b/silkaj/blockchain/information.py
@@ -13,7 +13,7 @@
 # You should have received a copy of the GNU Affero General Public License
 # along with Silkaj. If not, see <https://www.gnu.org/licenses/>.
 
-import arrow
+import pendulum
 import rich_click as click
 
 from silkaj.blockchain.tools import get_head_block
@@ -26,8 +26,8 @@ from silkaj.tools import get_currency_symbol
 def currency_info() -> None:
     head_block = get_head_block()
     ep = determine_endpoint()
-    current_time = arrow.get(head_block["time"]).to("local")
-    mediantime = arrow.get(head_block["medianTime"]).to("local")
+    current_time = pendulum.from_timestamp(head_block["time"], tz="local")
+    mediantime = pendulum.from_timestamp(head_block["medianTime"], tz="local")
     print(
         "Connected to node:",
         ep.host,
@@ -45,5 +45,5 @@ def currency_info() -> None:
         "\nMedian time:",
         mediantime.format(ALL),
         "\nDifference time:",
-        current_time - mediantime,
+        current_time.diff_for_humans(mediantime, True),
     )
diff --git a/silkaj/constants.py b/silkaj/constants.py
index 10b48d9230c803a91659dd1c438f0d8eb5762e50..01a762b02e526f45f1bf72f5a5a482fbb58e6acd 100644
--- a/silkaj/constants.py
+++ b/silkaj/constants.py
@@ -39,10 +39,10 @@ MINIMAL_RELATIVE_TX_AMOUNT = 1e-6
 CENT_MULT_TO_UNIT = 100
 SHORT_PUBKEY_SIZE = 8
 
-# Arrow date time formats
-# https://arrow.readthedocs.io/en/latest/guide.html#supported-tokens
-DATE = "MMMM D, YYYY"
-ALL = "MMMM D, YYYY hh:mm A ZZZ"
+# pendulum constants
+# see https://pendulum.eustace.io/docs/#localized-formats
+DATE = "LL"
+HOUR = "LTS"
+ALL = "LLL"
 # Not ISO 8601 compliant but common
 ALL_DIGITAL = "YYYY-MM-DD HH:mm:ss"
-FULL_HUMAN_FORMAT = "dddd D MMMM YYYY HH:mm ZZZ"
diff --git a/silkaj/money/history.py b/silkaj/money/history.py
index 93fb1059156539f18da3cd21503c5c3d49516a16..b1de80d7ef063e2299f01799a20f8d7db10fbf69 100644
--- a/silkaj/money/history.py
+++ b/silkaj/money/history.py
@@ -19,7 +19,7 @@ from pathlib import Path
 from typing import Any, Optional
 from urllib.error import HTTPError
 
-import arrow
+import pendulum
 import rich_click as click
 from duniterpy.api.bma.tx import history
 from duniterpy.api.client import Client
@@ -109,7 +109,7 @@ def generate_header(pubkey: str, currency_symbol: str, ud_value: int) -> str:
         idty = {"uid": ""}
     balance = mt.get_amount_from_pubkey(pubkey)
     balance_ud = round(balance[1] / ud_value, 2)
-    date = arrow.now().format(ALL)
+    date = pendulum.now().format(ALL)
     return f"Transactions history from: {idty['uid']} {gen_pubkey_checksum(pubkey)}\n\
 Current balance: {balance[1] / CENT_MULT_TO_UNIT} {currency_symbol}, {balance_ud} UD {currency_symbol} on {date}\n"
 
@@ -202,7 +202,9 @@ def parse_received_tx(
     identities = wt.identities_from_pubkeys(issuers, uids)
     for received_tx in received_txs:
         tx_list = []
-        tx_list.append(arrow.get(received_tx.time).to("local").format(ALL_DIGITAL))
+        tx_list.append(
+            pendulum.from_timestamp(received_tx.time, tz="local").format(ALL_DIGITAL)
+        )
         tx_list.append("")
         for i, issuer in enumerate(received_tx.issuers):
             tx_list[1] += prefix(None, None, i) + assign_idty_from_pubkey(
@@ -243,7 +245,9 @@ def parse_sent_tx(
     identities = wt.identities_from_pubkeys(pubkeys, uids)
     for sent_tx in sent_txs:
         tx_list = []
-        tx_list.append(arrow.get(sent_tx.time).to("local").format(ALL_DIGITAL))
+        tx_list.append(
+            pendulum.from_timestamp(sent_tx.time, tz="local").format(ALL_DIGITAL)
+        )
 
         total_amount, outputs = tx_amount(sent_tx, pubkey, sent_func)
         if len(outputs) > 1:
diff --git a/silkaj/wot/certify.py b/silkaj/wot/certify.py
index be07e9939bf50daaeb931aeddfb2e3f61b42d5ab..a2af158984364ae673fac1e18b5642c51b1f4da3 100644
--- a/silkaj/wot/certify.py
+++ b/silkaj/wot/certify.py
@@ -15,7 +15,7 @@
 
 import sys
 
-import arrow
+import pendulum
 import rich_click as click
 from duniterpy.api import bma
 from duniterpy.api.client import Client
@@ -103,7 +103,7 @@ def pre_checks(client: Client, issuer_pubkey: str, pubkey_to_certify: str) -> di
             # ĞT: 0<->4.8m - 4.8m + 12.5d
             renewable = cert["expiresIn"] - params["sigValidity"] + params["sigReplay"]
             if renewable > 0:
-                renewable_date = arrow.now().shift(seconds=renewable).format(DATE)
+                renewable_date = pendulum.now().add(seconds=renewable).format(DATE)
                 sys.exit(f"Certification renewable from {renewable_date}")
 
     # Check if the certification is already in the pending certifications
@@ -125,7 +125,7 @@ def certification_confirmation(
     idty_timestamp = idty_to_certify["meta"]["timestamp"]
     block_id_idty = get_block_id(idty_timestamp)
     block = client(bma.blockchain.block, block_id_idty.number)
-    timestamp_date = arrow.get(block["time"]).to("local").format(ALL)
+    timestamp_date = pendulum.from_timestamp(block["time"], tz="local").format(ALL)
     block_id_date = f": #{idty_timestamp[:15]}… {timestamp_date}"
     cert.append(["ID", issuer["uid"], "->", idty_to_certify["uid"] + block_id_date])
     cert.append(
@@ -137,8 +137,8 @@ def certification_confirmation(
         ],
     )
     params = bc_tools.get_blockchain_parameters()
-    cert_ends = arrow.now().shift(seconds=params["sigValidity"]).format(DATE)
-    cert.append(["Valid", arrow.now().format(DATE), "—>", cert_ends])
+    cert_ends = pendulum.now().add(seconds=params["sigValidity"]).format(DATE)
+    cert.append(["Valid", pendulum.now().format(DATE), "—>", cert_ends])
 
     table = tui.Table()
     table.fill_rows(
diff --git a/silkaj/wot/exclusions.py b/silkaj/wot/exclusions.py
index 1a65ca58e213837acc9f116e65379c1e69ec7dcb..1b700ffa523a1d4e437e06281834fc50f6f10145 100644
--- a/silkaj/wot/exclusions.py
+++ b/silkaj/wot/exclusions.py
@@ -19,7 +19,7 @@ import sys
 import time
 import urllib
 
-import arrow
+import pendulum
 import rich_click as click
 from duniterpy import constants as dp_const
 from duniterpy.api.bma import blockchain
@@ -222,8 +222,8 @@ def generate_identity_info(lookup, block, params):
     for i, certified in enumerate(lookup["signed"]):
         info += elements_inbetween_list(i, lookup["signed"])
         info += "@" + certified["uid"]
-    dt = arrow.get(block.mediantime).shift(hours=1).to(tz="local")
-    info += ".\n- **Exclu·e le** " + dt.format(constants.FULL_HUMAN_FORMAT, locale="fr")
+    dt = pendulum.from_timestamp(block.mediantime + constants.ONE_HOUR, tz="local")
+    info += ".\n- **Exclu·e le** " + dt.format("LLLL zz", locale="fr")
     info += "\n- **Raison de l'exclusion** : "
     if nbr_different_certifiers < params["sigQty"]:
         info += "manque de certifications"
diff --git a/silkaj/wot/idty_tools.py b/silkaj/wot/idty_tools.py
index 2febdafb6323795e4e7d983b3d67b1fd563285ae..723c28ff8e8e4287f27e5481ab5c9907b99d8ba3 100644
--- a/silkaj/wot/idty_tools.py
+++ b/silkaj/wot/idty_tools.py
@@ -18,7 +18,7 @@ import sys
 import urllib
 from typing import Union
 
-import arrow
+import pendulum
 import rich_click as click
 from duniterpy.api import bma
 from duniterpy.documents import BlockID, Identity, Revocation
@@ -40,7 +40,9 @@ def display_identity(idty: Identity) -> Texttable:
     id_table.append(["User ID", idty.uid])
     id_table.append(["Blockstamp", str(idty.block_id)])
     creation_block = client(bma.blockchain.block, idty.block_id.number)
-    creation_date = arrow.get(creation_block["time"]).to("local").format(ALL)
+    creation_date = pendulum.from_timestamp(creation_block["time"], tz="local").format(
+        ALL,
+    )
     id_table.append(["Created on", creation_date])
     # display infos
     table = Texttable(max_width=shutil.get_terminal_size().columns)
diff --git a/silkaj/wot/membership.py b/silkaj/wot/membership.py
index 92f7013d7d654df0486a3fd3342dc48b98c0ffa5..c48b0b4e1dd7fd422abbaa896e37401f31fa762a 100644
--- a/silkaj/wot/membership.py
+++ b/silkaj/wot/membership.py
@@ -15,7 +15,7 @@
 
 import logging
 
-import arrow
+import pendulum
 import rich_click as click
 from duniterpy.api import bma
 from duniterpy.documents import BlockID, Membership, get_block_id
@@ -104,7 +104,7 @@ def display_confirmation_table(
 
     table = []
     if membership_expires:
-        expires = arrow.now().shift(seconds=membership_expires).humanize()
+        expires = pendulum.now().add(seconds=membership_expires).diff_for_humans()
         table.append(["Expiration date of current membership", expires])
 
     if pending_memberships:
@@ -118,7 +118,7 @@ def display_confirmation_table(
         table.append(
             [
                 "Pending membership documents will expire",
-                arrow.now().shift(seconds=pending_expires).humanize(),
+                pendulum.now().add(seconds=pending_expires).diff_for_humans(),
             ],
         )
 
@@ -131,7 +131,7 @@ def display_confirmation_table(
     table.append(
         [
             "Identity published",
-            arrow.get(block["time"]).to("local").format(DATE),
+            pendulum.from_timestamp(block["time"], tz="local").format(DATE),
         ],
     )
 
@@ -139,14 +139,14 @@ def display_confirmation_table(
     table.append(
         [
             "Expiration date of new membership",
-            arrow.now().shift(seconds=params["msValidity"]).humanize(),
+            pendulum.now().add(seconds=params["msValidity"]).diff_for_humans(),
         ],
     )
 
     table.append(
         [
             "Expiration date of new membership from the mempool",
-            arrow.now().shift(seconds=params["msPeriod"]).humanize(),
+            pendulum.now().add(seconds=params["msPeriod"]).diff_for_humans(),
         ],
     )
 
diff --git a/silkaj/wot/status.py b/silkaj/wot/status.py
index e0fb2630883c341a45fd5e9e662800d7205ba7ed..fda49eb8eb6ee629d0f0dd088fe953772b8cf5a8 100644
--- a/silkaj/wot/status.py
+++ b/silkaj/wot/status.py
@@ -13,7 +13,7 @@
 # You should have received a copy of the GNU Affero General Public License
 # along with Silkaj. If not, see <https://www.gnu.org/licenses/>.
 
-import arrow
+import pendulum
 import rich_click as click
 from duniterpy.api.bma import blockchain, wot
 
@@ -58,7 +58,7 @@ def status(uid_pubkey: str) -> None:
         for req_cert in req["certifications"]:
             if req_cert["from"] == lookup_cert["pubkey"]:
                 certifications["received_expire"].append(
-                    arrow.now().shift(seconds=req_cert["expiresIn"]).format(DATE),
+                    pendulum.now().add(seconds=req_cert["expiresIn"]).format(DATE),
                 )
                 certifications["received"].append(f"{lookup_cert['uids'][0]} ✔")
                 break
@@ -67,7 +67,9 @@ def status(uid_pubkey: str) -> None:
             f"{(wt.identity_of(pending_cert['from']))['uid']} ✘",
         )
         certifications["received_expire"].append(
-            arrow.get(pending_cert["expires_on"]).to("local").format(DATE),
+            pendulum.from_timestamp(pending_cert["expires_on"], tz="local").format(
+                DATE
+            ),
         )
     certifications["sent"], certifications["sent_expire"] = get_sent_certifications(
         signed,
@@ -103,13 +105,15 @@ def membership_status(certifications: dict, pubkey: str, req: dict) -> None:
     is_member = bool(member_lookup)
     print("member:", is_member)
     if req["revoked"]:
-        revoke_date = arrow.get(req["revoked_on"]).to("local").format(DATE)
+        revoke_date = pendulum.from_timestamp(req["revoked_on"], tz="local").format(
+            DATE
+        )
         print(f"revoked: {req['revoked']}\nrevoked on: {revoke_date}")
     if not is_member and req["wasMember"]:
         print("expired:", req["expired"], "\nwasMember:", req["wasMember"])
     elif is_member:
         expiration_date = (
-            arrow.now().shift(seconds=req["membershipExpiresIn"]).format(DATE)
+            pendulum.now().add(seconds=req["membershipExpiresIn"]).format(DATE)
         )
         print(f"Membership document expiration: {expiration_date}")
         print("Sentry:", req["isSentry"])
@@ -145,7 +149,7 @@ def expiration_date_from_block_id(
         date_approximation(block_id, time_first_block, params["avgGenTime"])
         + params["sigValidity"]
     )
-    return arrow.get(expir_timestamp).to("local").format(DATE)
+    return pendulum.from_timestamp(expir_timestamp, tz="local").format(DATE)
 
 
 def date_approximation(block_id, time_first_block, avgentime):
diff --git a/tests/unit/money/test_history.py b/tests/unit/money/test_history.py
index 78d71a1b29110bdb29f17a19817c5a79b2078a92..5c18edbc316bf1e555ef8f159b19b323971b96d4 100644
--- a/tests/unit/money/test_history.py
+++ b/tests/unit/money/test_history.py
@@ -16,7 +16,7 @@
 import csv
 from pathlib import Path
 
-import arrow
+import pendulum
 import pytest
 from click.testing import CliRunner
 
@@ -183,28 +183,28 @@ relative_amount = str(round(CENT_MULT_TO_UNIT / mock_ud_value, 2))
 csv_reference = (
     ["Date", "Issuers/Recipients", "Amounts Ğ1", "Amounts UDĞ1", "Reference"],
     [
-        arrow.get(111111114).to("local").format(ALL_DIGITAL),
+        pendulum.from_timestamp(111111114, tz="local").format(ALL_DIGITAL),
         "CvrMiUhAJpNyX5sdAyZqPE6yEFfSsf6j9EpMmeKvMCWW:DNB",
         "1.0",
         relative_amount,
         "initialisation",
     ],
     [
-        arrow.get(111111113).to("local").format(ALL_DIGITAL),
+        pendulum.from_timestamp(111111113, tz="local").format(ALL_DIGITAL),
         "CmFKubyqbmJWbhyH2eEPVSSs4H4NeXGDfrETzEnRFtPd:CQ5",
         "1.0",
         relative_amount,
         "",
     ],
     [
-        arrow.get(111111112).to("local").format(ALL_DIGITAL),
+        pendulum.from_timestamp(111111112, tz="local").format(ALL_DIGITAL),
         "CvrMiUhAJpNyX5sdAyZqPE6yEFfSsf6j9EpMmeKvMCWW:DNB",
         "-1.0",
         f"-{relative_amount}",
         "",
     ],
     [
-        arrow.get(111111111).to("local").format(ALL_DIGITAL),
+        pendulum.from_timestamp(111111111, tz="local").format(ALL_DIGITAL),
         "CmFKubyqbmJWbhyH2eEPVSSs4H4NeXGDfrETzEnRFtPd:CQ5",
         "-1.0",
         f"-{relative_amount}",
diff --git a/tests/unit/wot/test_idty_tools.py b/tests/unit/wot/test_idty_tools.py
index 4451994b0e8ee54ae12961352e6e5c98897b2b23..3554fba7e082511ec30c8e0a969c238ec3eb63e9 100644
--- a/tests/unit/wot/test_idty_tools.py
+++ b/tests/unit/wot/test_idty_tools.py
@@ -16,7 +16,7 @@
 import re
 import urllib
 
-import arrow
+import pendulum
 import pytest
 from duniterpy.api import bma
 from duniterpy.documents.block_id import BlockID
@@ -294,7 +294,7 @@ def test_display_identity(idty, monkeypatch, capsys):
     assert "| User ID    | Claude" in result
     assert "| Blockstamp | 597334" in result
     # idty_block returns a block at timestamp 1594980185,.
-    creation_time = arrow.get(idty_block["time"]).to("local").format(ALL)
+    creation_time = pendulum.from_timestamp(idty_block["time"], tz="local").format(ALL)
     assert creation_time in result
 
 
diff --git a/tests/unit/wot/test_membership.py b/tests/unit/wot/test_membership.py
index b55f81d840bbc19eb5a240097cb7315e28e615e1..eac0fd3e267ff1dd38456b9baf4e17de200c3045 100644
--- a/tests/unit/wot/test_membership.py
+++ b/tests/unit/wot/test_membership.py
@@ -13,7 +13,7 @@
 # You should have received a copy of the GNU Affero General Public License
 # along with Silkaj. If not, see <https://www.gnu.org/licenses/>.
 
-import arrow
+import pendulum
 import pytest
 from duniterpy.api import bma
 from duniterpy.documents import Membership, get_block_id
@@ -73,7 +73,7 @@ def test_display_confirmation_table(patched_wot_requirements, monkeypatch, capsy
         table.append(
             [
                 "Expiration date of current membership",
-                arrow.now().shift(seconds=membership_expires).humanize(),
+                pendulum.now().add(seconds=membership_expires).diff_for_humans(),
             ],
         )
 
@@ -87,7 +87,7 @@ def test_display_confirmation_table(patched_wot_requirements, monkeypatch, capsy
         table.append(
             [
                 "Pending membership documents will expire",
-                arrow.now().shift(seconds=pending_expires).humanize(),
+                pendulum.now().add(seconds=pending_expires).diff_for_humans(),
             ],
         )
 
@@ -100,7 +100,7 @@ def test_display_confirmation_table(patched_wot_requirements, monkeypatch, capsy
     table.append(
         [
             "Identity published",
-            arrow.get(block["time"]).to("local").format(DATE),
+            pendulum.from_timestamp(block["time"], tz="local").format(DATE),
         ],
     )
 
@@ -108,14 +108,14 @@ def test_display_confirmation_table(patched_wot_requirements, monkeypatch, capsy
     table.append(
         [
             "Expiration date of new membership",
-            arrow.now().shift(seconds=params["msValidity"]).humanize(),
+            pendulum.now().add(seconds=params["msValidity"]).diff_for_humans(),
         ],
     )
 
     table.append(
         [
             "Expiration date of new membership from the mempool",
-            arrow.now().shift(seconds=params["msPeriod"]).humanize(),
+            pendulum.now().add(seconds=params["msPeriod"]).diff_for_humans(),
         ],
     )