diff --git a/src/wire/CMakeLists.txt b/src/wire/CMakeLists.txt
index 4a09e65..a1cc14d 100644
--- a/src/wire/CMakeLists.txt
+++ b/src/wire/CMakeLists.txt
@@ -34,3 +34,4 @@ target_include_directories(monero-lws-wire PUBLIC "${LMDB_INCLUDE}")
 target_link_libraries(monero-lws-wire PRIVATE monero::libraries)
 
 add_subdirectory(json)
+add_subdirectory(msgpack)
diff --git a/src/wire/field.h b/src/wire/field.h
index 8b2f435..5e1c112 100644
--- a/src/wire/field.h
+++ b/src/wire/field.h
@@ -1,4 +1,4 @@
-// Copyright (c) 2020, The Monero Project
+// Copyright (c) 2020-2023, The Monero Project
 // All rights reserved.
 //
 // Redistribution and use in source and binary forms, with or without modification, are
@@ -33,9 +33,13 @@
 #include "wire/filters.h"
 #include "wire/traits.h"
 
+//! A required field with the same key name and C/C++ name
+#define WIRE_FIELD_ID(id, name)                       \
+  ::wire::field< id >( #name , std::ref( self . name ))
+
 //! A required field has the same key name and C/C++ name
-#define WIRE_FIELD(name)                                \
-  ::wire::field( #name , std::ref( self . name ))
+#define WIRE_FIELD(name) \
+  WIRE_FIELD_ID(0, name)
 
 //! A required field has the same key name and C/C++ name AND is cheap to copy (faster output).
 #define WIRE_FIELD_COPY(name)                   \
@@ -61,12 +65,13 @@ namespace wire
 
 
   //! Links `name` to a `value` for object serialization.
-  template<typename T, bool Required>
+  template<typename T, bool Required, unsigned I = 0>
   struct field_
   {
     using value_type = typename unwrap_reference<T>::type;
     static constexpr bool is_required() noexcept { return Required; }
     static constexpr std::size_t count() noexcept { return 1; }
+    static constexpr unsigned id() noexcept { return I; }
 
     const char* name;
     T value;
@@ -85,15 +90,15 @@ namespace wire
   };
 
   //! Links `name` to `value`. Use `std::ref` if de-serializing.
-  template<typename T>
-  constexpr inline field_<T, true> field(const char* name, T value)
+  template<unsigned I = 0, typename T = void>
+  constexpr inline field_<T, true, I> field(const char* name, T value)
   {
     return {name, std::move(value)};
   }
 
   //! Links `name` to `value`. Use `std::ref` if de-serializing.
-  template<typename T>
-  constexpr inline field_<T, false> optional_field(const char* name, T value)
+  template<unsigned I = 0, typename T = void>
+  constexpr inline field_<T, false, I> optional_field(const char* name, T value)
   {
     return {name, std::move(value)};
   }
@@ -103,6 +108,7 @@ namespace wire
   template<typename T>
   struct option
   {
+    static constexpr unsigned id() noexcept { return 0; }
     const char* name;
   };
 
@@ -243,13 +249,13 @@ namespace wire
   }
 
 
-  template<typename T>
-  inline constexpr bool available(const field_<T, true>&) noexcept
+  template<typename T, unsigned I>
+  inline constexpr bool available(const field_<T, true, I>&) noexcept
   {
     return true;
   }
-  template<typename T>
-  inline bool available(const field_<T, false>& elem)
+  template<typename T, unsigned I>
+  inline bool available(const field_<T, false, I>& elem)
   {
     return bool(elem.get_value());
   }
diff --git a/src/wire/msgpack.h b/src/wire/msgpack.h
new file mode 100644
index 0000000..946d1b6
--- /dev/null
+++ b/src/wire/msgpack.h
@@ -0,0 +1,54 @@
+// Copyright (c) 2023, The Monero Project
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without modification, are
+// permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this list of
+//    conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice, this list
+//    of conditions and the following disclaimer in the documentation and/or other
+//    materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its contributors may be
+//    used to endorse or promote products derived from this software without specific
+//    prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
+// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
+// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
+// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#pragma once
+
+#include "wire/msgpack/base.h"
+#include "wire/msgpack/error.h"
+#include "wire/msgpack/read.h"
+#include "wire/msgpack/write.h"
+
+#define WIRE_MSGPACK_DEFINE_ENUM(type, map)                         \
+  void read_bytes(::wire::msgpack_reader& source, type& dest)       \
+  {                                                                 \
+    dest = type(source.enumeration(map));                           \
+  }                                                                 \
+  void write_bytes(::wire::msgpack_writer& dest, const type source) \
+  {                                                                 \
+    dest.enumeration(std::size_t(source), map);                     \
+  }
+
+#define WIRE_MSGPACK_DEFINE_OBJECT(type, map)                        \
+  void read_bytes(::wire::msgpack_reader& source, type& dest)        \
+  {                                                                  \
+    map(source, dest);                                               \
+  }                                                                  \
+  void write_bytes(::wire::msgpack_writer& dest, const type& source) \
+  {                                                                  \
+    map(dest, source);                                               \
+  }
+
diff --git a/src/wire/msgpack/CMakeLists.txt b/src/wire/msgpack/CMakeLists.txt
new file mode 100644
index 0000000..7ad3c63
--- /dev/null
+++ b/src/wire/msgpack/CMakeLists.txt
@@ -0,0 +1,33 @@
+# Copyright (c) 2023, The Monero Project
+#
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without modification, are
+# permitted provided that the following conditions are met:
+#
+# 1. Redistributions of source code must retain the above copyright notice, this list of
+#    conditions and the following disclaimer.
+#
+# 2. Redistributions in binary form must reproduce the above copyright notice, this list
+#    of conditions and the following disclaimer in the documentation and/or other
+#    materials provided with the distribution.
+#
+# 3. Neither the name of the copyright holder nor the names of its contributors may be
+#    used to endorse or promote products derived from this software without specific
+#    prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
+# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
+# THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+# STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
+# THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+set(monero-lws_wire-msgpack_sources error.cpp read.cpp write.cpp)
+set(monero-lws_wire-msgpack_headers base.h error.h fwd.h read.h write.h)
+
+add_library(monero-lws-wire-msgpack ${monero-lws_wire-msgpack_sources} ${monero-lws-wire-msgpack_headers})
+target_link_libraries(monero-lws-wire-msgpack  monero::libraries monero-lws-wire)
diff --git a/src/wire/msgpack/base.h b/src/wire/msgpack/base.h
new file mode 100644
index 0000000..5e0c2cb
--- /dev/null
+++ b/src/wire/msgpack/base.h
@@ -0,0 +1,171 @@
+// Copyright (c) 2023, The Monero Project
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without modification, are
+// permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this list of
+//    conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice, this list
+//    of conditions and the following disclaimer in the documentation and/or other
+//    materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its contributors may be
+//    used to endorse or promote products derived from this software without specific
+//    prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
+// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
+// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
+// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#pragma once
+
+#include <cstdint>
+#include <string>
+#include <tuple>
+
+#include "byte_slice.h"
+#include "common/expect.h"
+#include "wire/msgpack/fwd.h"
+
+namespace wire
+{
+  struct msgpack
+  {
+    using input_type = msgpack_reader;
+    using output_type = msgpack_writer;
+
+    //! Tags that do not require bitmask to identify
+    enum class tag : std::uint8_t
+    {
+      nil = 0xc0,
+      unused,
+      False,
+      True,
+      binary8,
+      binary16,
+      binary32,
+      extension8,
+      extension16,
+      extension32,
+      float32,
+      float64,
+      uint8,
+      uint16,
+      uint32,
+      uint64,
+      int8,
+      int16,
+      int32,
+      int64,
+      fixed_extension1,
+      fixed_extension2,
+      fixed_extension4,
+      fixed_extension8,
+      fixed_extension16,
+      string8,
+      string16,
+      string32,
+      array16,
+      array32,
+      object16,
+      object32
+    };
+
+    //! Link a fixed tag `T` to its corresponding mask `M` and max value `N`
+    template<std::uint8_t T, std::uint8_t M, std::uint8_t N>
+    struct fixed_tag
+    {
+      static constexpr std::uint8_t tag() noexcept { return T; }
+      static constexpr std::uint8_t mask() noexcept { return M; }
+      static constexpr std::uint8_t max() noexcept { return N; }
+
+      //! \return True if `value` is fixed tag `T`
+      static constexpr bool matches(const std::uint8_t value) noexcept
+      { return (value & mask()) == tag(); }
+
+      //! \return True if `value` is fixed tag `T`
+      static constexpr bool matches(const msgpack::tag value) noexcept
+      { return matches(std::uint8_t(value)); }
+
+      //! \return Value encoded in fixed tag
+      static constexpr std::uint8_t extract(const std::uint8_t value) noexcept
+      { return value & ~mask(); }
+
+      //! \return Value encoded in fixed tag
+      static constexpr std::uint8_t extract(const msgpack::tag value) noexcept
+      { return extract(std::uint8_t(value)); }
+    };
+
+    // Tags requiring bitmask to identify
+    using ftag_unsigned = fixed_tag<0x00, 0x80, 0x7f>;
+    using ftag_signed =   fixed_tag<0xe0, 0xe0, 0>;
+    using ftag_string =   fixed_tag<0xa0, 0xe0, 31>;
+    using ftag_array =    fixed_tag<0x90, 0xf0, 15>;
+    using ftag_object =   fixed_tag<0x80, 0xf0, 15>;
+
+    //! Link a msgpack tag to a C++ numeric
+    template<typename T, tag V>
+    struct type
+    {
+      static constexpr bool is_signed() noexcept { return std::numeric_limits<T>::is_signed; }
+      static constexpr T min() noexcept { return std::numeric_limits<T>::min(); }
+      static constexpr T max() noexcept { return std::numeric_limits<T>::max(); }
+      static constexpr tag Tag() noexcept { return V; }
+    };
+
+    using int8 = type<std::int8_t,   tag::int8>;
+    using int16 = type<std::int16_t, tag::int16>;
+    using int32 = type<std::int32_t, tag::int32>;
+    using int64 = type<std::int64_t, tag::int64>;
+    using signed_types = std::tuple<int8, int16, int32, int64>;
+
+    using uint8 = type<std::uint8_t,   tag::uint8>;
+    using uint16 = type<std::uint16_t, tag::uint16>;
+    using uint32 = type<std::uint32_t, tag::uint32>;
+    using uint64 = type<std::uint64_t, tag::uint64>;
+    using unsigned_types = std::tuple<uint8, uint16, uint32, uint64>;
+
+    using integer_types = std::tuple<
+      msgpack::uint8, msgpack::int8, msgpack::uint16, msgpack::int16,
+      msgpack::uint32, msgpack::int32, msgpack::uint64, msgpack::int64
+    >;
+
+    using string8 = type<std::uint8_t,   tag::string8>;
+    using string16 = type<std::uint16_t, tag::string16>;
+    using string32 = type<std::uint32_t, tag::string32>;
+    using string_types = std::tuple<string8, string16, string32>;
+
+    using binary8 = type<std::uint8_t,   tag::binary8>;
+    using binary16 = type<std::uint16_t, tag::binary16>;
+    using binary32 = type<std::uint32_t, tag::binary32>;
+    using binary_types = std::tuple<binary8, binary16, binary32>;
+
+    using extension8 = type<std::uint8_t,   tag::extension8>;
+    using extension16 = type<std::uint16_t, tag::extension16>;
+    using extension32 = type<std::uint32_t, tag::extension32>;
+    using extension_types = std::tuple<extension8, extension16, extension32>;
+
+    using array16 = type<std::uint16_t, tag::array16>;
+    using array32 = type<std::uint32_t, tag::array32>;
+    using array_types = std::tuple<array16, array32>;
+
+    using object16 = type<std::uint16_t, tag::object16>;
+    using object32 = type<std::uint32_t, tag::object32>;
+    using object_types = std::tuple<object16, object32>;
+
+    template<typename T>
+    static expect<T> from_bytes(epee::byte_slice&& source);
+
+    template<typename T>
+    static epee::byte_slice to_bytes(const T& source);
+  };
+}
+
diff --git a/src/wire/msgpack/error.cpp b/src/wire/msgpack/error.cpp
new file mode 100644
index 0000000..a1278dd
--- /dev/null
+++ b/src/wire/msgpack/error.cpp
@@ -0,0 +1,71 @@
+// Copyright (c) 2023, The Monero Project
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without modification, are
+// permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this list of
+//    conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice, this list
+//    of conditions and the following disclaimer in the documentation and/or other
+//    materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its contributors may be
+//    used to endorse or promote products derived from this software without specific
+//    prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
+// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
+// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
+// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "error.h"
+
+namespace wire
+{
+namespace error
+{
+  const char* get_string(const msgpack value) noexcept
+  {
+    switch (value)
+    {
+      default:
+        break;
+      case msgpack::incomplete:
+        return "Incomplete msgpack tree structure";
+      case msgpack::integer_encoding:
+        return "Unable to encode integer in msgpack";
+      case msgpack::invalid:
+        return "Invalid msgpack encoding";
+      case msgpack::not_enough_bytes:
+        return "Expected more bytes in the msgpack stream";
+    }
+
+    return "Unknown msgpack error";
+  }
+
+  const std::error_category& msgpack_category() noexcept
+  {
+    struct category final : std::error_category
+    {
+      virtual const char* name() const noexcept override final
+      {
+        return "wire::error::msgpack_category()";
+      }
+
+      virtual std::string message(int value) const override final
+      {
+        return get_string(msgpack(value));
+      }
+    };
+    static const category instance{};
+    return instance;
+  }
+} // error
+} // wire
diff --git a/src/wire/msgpack/error.h b/src/wire/msgpack/error.h
new file mode 100644
index 0000000..345d047
--- /dev/null
+++ b/src/wire/msgpack/error.h
@@ -0,0 +1,59 @@
+// Copyright (c) 2023, The Monero Project
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without modification, are
+// permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this list of
+//    conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice, this list
+//    of conditions and the following disclaimer in the documentation and/or other
+//    materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its contributors may be
+//    used to endorse or promote products derived from this software without specific
+//    prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
+// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
+// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
+// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#pragma once
+
+#include <system_error>
+
+namespace wire
+{
+namespace error
+{
+  //! Type wrapper to "grab" rapidjson errors
+  enum class msgpack : int
+  {
+    success = 0, // required for `expected<T>`
+    incomplete,
+    integer_encoding,
+    invalid,
+    not_enough_bytes
+  };
+
+  //! \return Static string describing error `value`.
+  const char* get_string(msgpack value) noexcept;
+
+  //! \return Category for msgpack generated errors.
+  const std::error_category& msgpack_category() noexcept;
+
+  //! \return Error code with `value` and `rapidjson_category()`.
+  inline std::error_code make_error_code(msgpack value) noexcept
+  {
+    return std::error_code{int(value), msgpack_category()};
+  }
+}
+}
+
diff --git a/src/wire/msgpack/fwd.h b/src/wire/msgpack/fwd.h
new file mode 100644
index 0000000..9363a73
--- /dev/null
+++ b/src/wire/msgpack/fwd.h
@@ -0,0 +1,45 @@
+// Copyright (c) 2023, The Monero Project
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without modification, are
+// permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this list of
+//    conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice, this list
+//    of conditions and the following disclaimer in the documentation and/or other
+//    materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its contributors may be
+//    used to endorse or promote products derived from this software without specific
+//    prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
+// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
+// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
+// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#pragma once
+
+#define WIRE_MSGPACK_DECLARE_ENUM(type)            \
+  const char* get_string(type) noexcept;           \
+  void read_bytes(::wire::msgpack_reader&, type&); \
+  void write_bytes(:wire::msgpack_writer&, type)
+
+#define WIRE_MSGPACK_DECLARE_OBJECT(type)                \
+  void read_bytes(::wire::msgpack_reader&, type&);       \
+  void write_bytes(::wire::msgpack_writer&, const type&)
+
+namespace wire
+{
+  struct msgpack;
+  class msgpack_reader;
+  class msgpack_writer;
+}
+
diff --git a/src/wire/msgpack/read.cpp b/src/wire/msgpack/read.cpp
new file mode 100644
index 0000000..2a11e02
--- /dev/null
+++ b/src/wire/msgpack/read.cpp
@@ -0,0 +1,517 @@
+// Copyright (c) 2023, The Monero Project
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without modification, are
+// permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this list of
+//    conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice, this list
+//    of conditions and the following disclaimer in the documentation and/or other
+//    materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its contributors may be
+//    used to endorse or promote products derived from this software without specific
+//    prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
+// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
+// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
+// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "read.h"
+
+#include <boost/endian/buffers.hpp>
+#include <boost/fusion/include/any.hpp>
+#include <boost/fusion/include/std_tuple.hpp>
+#include <boost/numeric/conversion/cast.hpp>
+#include <cstring>
+#include <limits>
+#include <stdexcept>
+#include <type_traits>
+
+#include "wire/error.h"
+#include "wire/msgpack/error.h"
+
+// Expands to every possible fixed string tag value
+#define MLWS_FIXED_STRING_TAGS()                                \
+  case wire::msgpack::tag(0xa0): case wire::msgpack::tag(0xa1): \
+  case wire::msgpack::tag(0xa2): case wire::msgpack::tag(0xa3): \
+  case wire::msgpack::tag(0xa4): case wire::msgpack::tag(0xa5): \
+  case wire::msgpack::tag(0xa6): case wire::msgpack::tag(0xa7): \
+  case wire::msgpack::tag(0xa8): case wire::msgpack::tag(0xa9): \
+  case wire::msgpack::tag(0xaa): case wire::msgpack::tag(0xab): \
+  case wire::msgpack::tag(0xac): case wire::msgpack::tag(0xad): \
+  case wire::msgpack::tag(0xae): case wire::msgpack::tag(0xaf): \
+  case wire::msgpack::tag(0xb0): case wire::msgpack::tag(0xb1): \
+  case wire::msgpack::tag(0xb2): case wire::msgpack::tag(0xb3): \
+  case wire::msgpack::tag(0xb4): case wire::msgpack::tag(0xb5): \
+  case wire::msgpack::tag(0xb6): case wire::msgpack::tag(0xb7): \
+  case wire::msgpack::tag(0xb8): case wire::msgpack::tag(0xb9): \
+  case wire::msgpack::tag(0xba): case wire::msgpack::tag(0xbb): \
+  case wire::msgpack::tag(0xbc): case wire::msgpack::tag(0xbd): \
+  case wire::msgpack::tag(0xbe): case wire::msgpack::tag(0xbf):
+
+namespace
+{
+  template<typename T>
+  using limits = std::numeric_limits<T>;
+
+  //! \return True iif `value` matches a tag in `T` tuple.
+  template<typename T>
+  bool matches(const wire::msgpack::tag tag)
+  {
+    const auto matched_type = [tag] (const auto type)
+    {
+      return type.Tag() == tag;
+    };
+    // NOTE: This is slower than a switch but more flexible/reusable
+    return boost::fusion::any(T{}, matched_type);
+  }
+
+  //! \return Integer `T` encoded as big endian in `source`.
+  template<typename T>
+  T read_endian(epee::byte_slice& source)
+  {
+    static_assert(std::is_integral<T>::value, "must be integral type");
+    static constexpr const std::size_t bits = 8 * sizeof(T);
+    using buffer_type =
+      boost::endian::endian_buffer<boost::endian::order::big, T, bits>;
+
+    buffer_type buffer;
+    static_assert(sizeof(buffer) == sizeof(T), "unexpected buffer size");
+    if (source.size() < sizeof(buffer))
+      WIRE_DLOG_THROW_(wire::error::msgpack::not_enough_bytes);
+    std::memcpy(std::addressof(buffer), source.data(), sizeof(buffer));
+    source.remove_prefix(sizeof(buffer));
+    return buffer.value();
+  }
+
+  //! \return Integer `T` encoded as big endian in `source`.
+  template<typename T, wire::msgpack::tag U>
+  T read_endian(epee::byte_slice& source, const wire::msgpack::type<T, U>)
+  { return read_endian<T>(source); }
+
+  //! \return Integer `T` whose encoding is specified by tag `next`
+  template<typename T>
+  T read_integer(epee::byte_slice& source, const wire::msgpack::tag next)
+  {
+    try
+    {
+      // msgpack::integer_types
+      switch (next)
+      {
+        default:
+          break;
+        case wire::msgpack::tag::int8:
+          return boost::numeric_cast<T>(read_endian<std::int8_t>(source));
+        case wire::msgpack::tag::uint8:
+          return boost::numeric_cast<T>(read_endian<std::uint8_t>(source));
+        case wire::msgpack::tag::int16:
+          return boost::numeric_cast<T>(read_endian<std::int16_t>(source));
+        case wire::msgpack::tag::uint16:
+          return boost::numeric_cast<T>(read_endian<std::uint16_t>(source));
+        case wire::msgpack::tag::int32:
+          return boost::numeric_cast<T>(read_endian<std::int32_t>(source));
+        case wire::msgpack::tag::uint32:
+          return boost::numeric_cast<T>(read_endian<std::uint32_t>(source));
+        case wire::msgpack::tag::int64:
+          return boost::numeric_cast<T>(read_endian<std::int64_t>(source));
+        case wire::msgpack::tag::uint64:
+          return boost::numeric_cast<T>(read_endian<std::uint64_t>(source));
+      }
+    }
+    catch (const boost::numeric::positive_overflow&)
+    { WIRE_DLOG_THROW_(wire::error::schema::smaller_integer); }
+    catch (const boost::numeric::negative_overflow&)
+    { WIRE_DLOG_THROW_(wire::error::schema::larger_integer); }
+    
+    WIRE_DLOG_THROW_(wire::error::schema::integer);
+  }
+
+  epee::byte_slice read_raw(epee::byte_slice& source, const std::size_t bytes)
+  {
+    if (source.size() < bytes)
+      WIRE_DLOG_THROW_(wire::error::msgpack::not_enough_bytes);
+    return source.take_slice(bytes);
+  }
+
+  template<typename T>
+  epee::byte_slice read_raw(epee::byte_slice& source)
+  {
+    return read_raw(source, wire::integer::cast_unsigned<std::size_t>(read_endian<T>(source)));
+  }
+
+  epee::byte_slice read_string(epee::byte_slice& source, const wire::msgpack::tag next)
+  {
+    switch (next)
+    {
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wswitch"
+      MLWS_FIXED_STRING_TAGS()
+        return read_raw(source, wire::msgpack::ftag_string::extract(next));
+#pragma GCC diagnostic pop
+      case wire::msgpack::tag::string8:
+        return read_raw<std::uint8_t>(source);
+      case wire::msgpack::tag::string16:
+        return read_raw<std::uint16_t>(source);
+      case wire::msgpack::tag::string32:
+        return read_raw<std::uint32_t>(source);
+      default:
+        break;
+    }
+    WIRE_DLOG_THROW_(wire::error::schema::string);
+  }
+
+  //! \return Binary blob encoded message
+  epee::byte_slice read_binary(epee::byte_slice& source, const wire::msgpack::tag next)
+  {
+    switch (next)
+    {
+      case wire::msgpack::tag::binary8:
+        return read_raw<std::uint8_t>(source);
+      case wire::msgpack::tag::binary16:
+        return read_raw<std::uint16_t>(source);
+      case wire::msgpack::tag::binary32:
+        return read_raw<std::uint32_t>(source);
+      default:
+        break;
+    }
+    WIRE_DLOG_THROW_(wire::error::schema::string);
+  }
+}
+
+namespace wire
+{
+  void msgpack_reader::throw_logic_error()
+  {
+    throw std::logic_error{"Bug in msgpack_reader usage"};
+  }
+
+  void msgpack_reader::skip_value()
+  {
+    assert(remaining_);
+    if (limits<std::size_t>::max() == remaining_)
+      throw std::runtime_error{"msgpack_reader exceeded tree tracking"};
+
+    const std::size_t initial = remaining_;
+    do
+    {
+      const std::size_t size = source_.size();
+      const msgpack::tag next = peek_tag();
+      switch (next)
+      {
+        default:
+          break;
+        case msgpack::tag::nil:
+        case msgpack::tag::unused:
+        case msgpack::tag::False:
+        case msgpack::tag::True:
+          source_.remove_prefix(1);
+          break;
+        case msgpack::tag::binary8:
+        case msgpack::tag::binary16:
+        case msgpack::tag::binary32:
+          source_.remove_prefix(1);
+          read_binary(source_, next);
+          break;
+        case msgpack::tag::extension8:
+          source_.remove_prefix(1);
+          read_raw<std::uint8_t>(source_);
+          source_.remove_prefix(1);
+          break;
+        case msgpack::tag::extension16:
+          source_.remove_prefix(1);
+          read_raw<std::uint16_t>(source_);
+          source_.remove_prefix(1);
+          break;
+        case msgpack::tag::extension32:
+          source_.remove_prefix(1);
+          read_raw<std::uint32_t>(source_);
+          source_.remove_prefix(1);
+          break;
+        case msgpack::tag::int8:
+        case msgpack::tag::uint8:
+          source_.remove_prefix(2);
+          break;
+        case msgpack::tag::int16:
+        case msgpack::tag::uint16:
+        case msgpack::tag::fixed_extension1:
+          source_.remove_prefix(3);
+          break;
+        case msgpack::tag::int32:
+        case msgpack::tag::uint32:
+        case msgpack::tag::float32:
+          source_.remove_prefix(5);
+          break;
+        case msgpack::tag::int64:
+        case msgpack::tag::uint64:
+        case msgpack::tag::float64:
+          source_.remove_prefix(9);
+          break;
+        case msgpack::tag::fixed_extension2:
+          source_.remove_prefix(4);
+          break;
+        case msgpack::tag::fixed_extension4:
+          source_.remove_prefix(6);
+          break;
+        case msgpack::tag::fixed_extension8:
+          source_.remove_prefix(10);
+          break;
+        case msgpack::tag::fixed_extension16:
+          source_.remove_prefix(18);
+          break;
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wswitch"
+        MLWS_FIXED_STRING_TAGS()
+        case msgpack::tag::string8:
+        case msgpack::tag::string16:
+        case msgpack::tag::string32:
+          source_.remove_prefix(1);
+          read_string(source_, next);
+          break;
+        case msgpack::tag(0x90): case msgpack::tag(0x91): case msgpack::tag(0x92):
+        case msgpack::tag(0x93): case msgpack::tag(0x94): case msgpack::tag(0x95):
+        case msgpack::tag(0x96): case msgpack::tag(0x97): case msgpack::tag(0x98):
+        case msgpack::tag(0x99): case msgpack::tag(0x9a): case msgpack::tag(0x9b):
+        case msgpack::tag(0x9c): case msgpack::tag(0x9d): case msgpack::tag(0x9e):
+        case msgpack::tag(0x9f):
+        case msgpack::tag::array16:
+        case msgpack::tag::array32:
+          start_array();
+          break;
+        case msgpack::tag(0x80): case msgpack::tag(0x81): case msgpack::tag(0x82):
+        case msgpack::tag(0x83): case msgpack::tag(0x84): case msgpack::tag(0x85):
+        case msgpack::tag(0x86): case msgpack::tag(0x87): case msgpack::tag(0x88):
+        case msgpack::tag(0x89): case msgpack::tag(0x8a): case msgpack::tag(0x8b):
+        case msgpack::tag(0x8c): case msgpack::tag(0x8d): case msgpack::tag(0x8e):
+        case msgpack::tag(0x8f):
+        case msgpack::tag::object16:
+        case msgpack::tag::object32:
+          start_object();
+          break;
+#pragma GCC diagnostic pop
+      };
+
+      if (size == source_.size())
+      {
+        if (!msgpack::ftag_unsigned::matches(next) && !msgpack::ftag_signed::matches(next))
+          WIRE_DLOG_THROW_(error::msgpack::invalid);
+        source_.remove_prefix(1);
+      }
+      update_remaining();
+    } while (initial <= remaining_);
+  }
+
+  msgpack::tag msgpack_reader::peek_tag()
+  {
+    if (source_.empty())
+      WIRE_DLOG_THROW_(error::msgpack::not_enough_bytes);
+    return msgpack::tag(*source_.data());
+  }
+
+  msgpack::tag msgpack_reader::get_tag()
+  {
+    const msgpack::tag next = peek_tag();
+    source_.remove_prefix(1);
+    return next;
+  }
+
+  std::intmax_t msgpack_reader::do_integer(const msgpack::tag next)
+  {
+    if (msgpack::ftag_signed::matches(next))
+      return *reinterpret_cast<const std::int8_t*>(std::addressof(next)); // special case
+    return read_integer<std::intmax_t>(source_, next);
+  }
+
+  std::uintmax_t msgpack_reader::do_unsigned_integer(const msgpack::tag next)
+  {
+    return read_integer<std::uintmax_t>(source_, next);
+  }
+
+  template<typename T, typename U>
+  std::size_t msgpack_reader::read_count(const error::schema expected)
+  {
+    const msgpack::tag next = get_tag();
+    if (T::matches(next))
+      return T::extract(next);
+
+    std::size_t out = 0;
+    const auto matched_type = [this, &out, next](const auto type)
+    {
+      if (type.Tag() == next)
+      {
+        out = integer::cast_unsigned<std::size_t>(read_endian(source_, type));
+        return true;
+      }
+      return false;
+    };
+
+    if (!boost::fusion::any(U{}, matched_type))
+      WIRE_DLOG_THROW_(expected);
+
+    return out;
+  }
+
+  void msgpack_reader::check_complete() const
+  {
+    if (remaining_)
+      WIRE_DLOG_THROW_(error::msgpack::incomplete);
+  }
+
+  bool msgpack_reader::boolean()
+  {
+    update_remaining();
+    switch (get_tag())
+    {
+      case msgpack::tag::True:
+        return true;
+      case msgpack::tag::False:
+        return false;
+      default:
+        break;
+    }
+    WIRE_DLOG_THROW_(error::schema::boolean);
+  }
+
+  double msgpack_reader::real()
+  {
+    update_remaining();
+
+    const auto read_float = [this](auto value)
+    {
+      if (source_.size() < sizeof(value))
+        WIRE_DLOG_THROW_(error::msgpack::not_enough_bytes);
+      std::memcpy(std::addressof(value), source_.data(), sizeof(value));
+      source_.remove_prefix(sizeof(value));
+      return value;
+    };
+
+    switch (get_tag())
+    {
+      case msgpack::tag::float32:
+        return read_float(float(0));
+      case msgpack::tag::float64:
+        return read_float(double(0));
+      default:
+        break;
+    }
+    WIRE_DLOG_THROW_(error::schema::number);
+  }
+
+  std::string msgpack_reader::string()
+  {
+    update_remaining();
+    const epee::byte_slice bytes = read_string(source_, get_tag());
+    return std::string{reinterpret_cast<const char*>(bytes.data()), bytes.size()};
+  }
+
+  std::vector<std::uint8_t> msgpack_reader::binary()
+  {
+    update_remaining();
+    const epee::byte_slice bytes = read_binary(source_, get_tag());
+    return std::vector<std::uint8_t>{bytes.begin(), bytes.end()};
+  }
+
+  void msgpack_reader::binary(epee::span<std::uint8_t> dest)
+  {
+    update_remaining();
+    const epee::byte_slice bytes = read_binary(source_, get_tag());
+    if (dest.size() != bytes.size())
+      WIRE_DLOG_THROW(error::schema::fixed_binary, "of size " << dest.size() << " but got " << bytes.size());
+    std::memcpy(dest.data(), bytes.data(), dest.size());
+  }
+
+  std::size_t msgpack_reader::enumeration(const epee::span<char const* const> enums)
+  {
+    const std::uintmax_t value = unsigned_integer();
+    if (enums.size() < value)
+      WIRE_DLOG_THROW(error::schema::enumeration, value << " is not a valid enum");
+    return std::size_t(value);
+  }
+
+  std::size_t msgpack_reader::start_array()
+  {
+    const std::size_t upcoming =
+      read_count<msgpack::ftag_array, msgpack::array_types>(error::schema::array);
+    if (limits<std::size_t>::max() - remaining_ < upcoming)
+      throw std::runtime_error{"Exceeded max tree tracking for msgpack_reader"};
+    remaining_ += upcoming;
+    return upcoming;
+  }
+
+  bool msgpack_reader::is_array_end(const std::size_t count)
+  {
+    if (count)
+      return false;
+    update_remaining();
+    return true;
+  }
+
+  std::size_t msgpack_reader::start_object()
+  {
+    const std::size_t upcoming =
+      read_count<msgpack::ftag_object, msgpack::object_types>(error::schema::object);
+    if (limits<std::size_t>::max() / 2 < upcoming)
+      throw std::runtime_error{"Exceeded max object tracking for msgpack_reader"};
+    if (limits<std::size_t>::max() - remaining_ < upcoming * 2)
+      throw std::runtime_error{"Exceeded msgpack_reader:: tree tracking"};
+    remaining_ += upcoming * 2;
+    return upcoming;
+  }
+
+  bool msgpack_reader::key(const epee::span<const key_map> map, std::size_t& state, std::size_t& index)
+  {
+    index = map.size();
+    for ( ;state; --state)
+    {
+      update_remaining(); // for key
+      const msgpack::tag next = get_tag();
+      const bool single = msgpack::ftag_unsigned::matches(next);
+      if (single || matches<msgpack::unsigned_types>(next))
+      {
+        unsigned key = std::uint8_t(next);
+        if (!single)
+          key = read_integer<unsigned>(source_, next);
+        for (const key_map& elem : map)
+        {
+          if (elem.id == key)
+          {
+            index = std::addressof(elem) - map.begin();
+            break;
+          }
+        }
+      }
+      else if (msgpack::ftag_string::matches(next) || matches<msgpack::string_types>(next))
+      {
+        const epee::byte_slice key = read_string(source_, next);
+        for (const key_map& elem : map)
+        {
+          const boost::string_ref elem_{elem.name};
+          if (key.size() == elem_.size() && std::memcmp(key.data(), elem_.data(), key.size()) == 0)
+          {
+            index = std::addressof(elem) - map.begin();
+            break;
+          }
+        }
+      }
+      else
+        WIRE_DLOG_THROW(error::schema::invalid_key, "Invalid key type");
+
+      if (index < map.size())
+      {
+        --state;
+        return true;
+      }
+      skip_value();
+    } // until state == 0
+    update_remaining(); // for end of object
+    return false;
+  }
+}
diff --git a/src/wire/msgpack/read.h b/src/wire/msgpack/read.h
new file mode 100644
index 0000000..df0b107
--- /dev/null
+++ b/src/wire/msgpack/read.h
@@ -0,0 +1,164 @@
+// Copyright (c) 2023, The Monero Project
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without modification, are
+// permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this list of
+//    conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice, this list
+//    of conditions and the following disclaimer in the documentation and/or other
+//    materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its contributors may be
+//    used to endorse or promote products derived from this software without specific
+//    prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
+// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
+// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
+// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#pragma once
+
+#include <boost/utility/string_ref.hpp>
+#include <cstddef>
+#include <string>
+#include <type_traits>
+#include <utility>
+#include <vector>
+
+#include "wire/field.h"
+#include "wire/msgpack/base.h"
+#include "wire/read.h"
+#include "wire/traits.h"
+
+namespace wire
+{
+  //! Reads MSGPACK tokens one-at-a-time for DOMless parsing
+  class msgpack_reader : public reader
+  {
+    epee::byte_slice source_;
+    std::size_t remaining_; //!< Expected number of elements remaining
+
+    //! \throw std::logic_error
+    [[noreturn]] void throw_logic_error();
+    //! Decrement remaining_ if not zero, \throw std::logic_error when `remaining_ == 0`.
+    void update_remaining()
+    {
+      if (remaining_)
+        --remaining_;
+      else
+        throw_logic_error();
+    }
+    
+    //! Skips next value. \throw wire::exception if invalid JSON syntax.
+    void skip_value();
+
+    //! \return Next tag but leave `source_` untouched.
+    msgpack::tag peek_tag();
+    //! \return Next tag and remove first byte from `source_`.
+    msgpack::tag get_tag();
+
+    //! \return Integer from `soure_` where positive fixed tag has been checked.
+    std::intmax_t do_integer(msgpack::tag);
+    //! \return Integer from `source_` where fixed tag has been checked.
+    std::uintmax_t do_unsigned_integer(msgpack::tag);
+
+    //! \return Number of items determined by `T` fixed tag and `U` tuple of tags.
+    template<typename T, typename U>
+    std::size_t read_count(error::schema);
+
+  public:
+    explicit msgpack_reader(epee::byte_slice&& source)
+      : reader(), source_(std::move(source)), remaining_(1)
+    {}
+
+    //! \throw wire::exception if JSON parsing is incomplete.
+    void check_complete() const override final;
+
+    //! \throw wire::exception if next token not a boolean.
+    bool boolean() override final;
+
+    //! \throw wire::expception if next token not an integer.
+    std::intmax_t integer() override final
+    {
+      update_remaining();
+      const msgpack::tag next = get_tag();
+      if (std::uint8_t(next) <= msgpack::ftag_unsigned::max())
+        return std::uint8_t(next);
+      return do_integer(next);
+    }
+
+    //! \throw wire::exception if next token not an unsigned integer.
+    std::uintmax_t unsigned_integer() override final
+    {
+      update_remaining();
+      const msgpack::tag next = get_tag();
+      if (std::uint8_t(next) <= msgpack::ftag_unsigned::max())
+        return std::uint8_t(next);
+      return do_unsigned_integer(next);
+    }
+
+    //! \throw wire::exception if next token not a valid real number
+    double real() override final;
+
+    //! \throw wire::exception if next token not a string
+    std::string string() override final;
+
+    //! \throw wire::exception if next token cannot be read as hex
+    std::vector<std::uint8_t> binary() override final;
+
+    //! \throw wire::exception if next token cannot be read as hex into `dest`.
+    void binary(epee::span<std::uint8_t> dest) override final;
+
+    //! \throw wire::exception if invalid next token invalid enum. \return Index in `enums`.
+    std::size_t enumeration(epee::span<char const* const> enums) override final;
+
+
+    //! \throw wire::exception if next token not `[`.
+    std::size_t start_array() override final;
+
+    //! \return true when `count == 0`.
+    bool is_array_end(const std::size_t count) override final;
+
+
+    //! \throw wire::exception if next token not `{`.
+    std::size_t start_object() override final;
+
+    /*! \throw wire::exception if next token not key or `}`.
+        \param[out] index of key match within `map`.
+        \return True if another value to read. */
+    bool key(epee::span<const key_map> map, std::size_t&, std::size_t& index) override final;
+  };
+
+
+  // Don't call `read` directly in this namespace, do it from `wire_read`.
+
+  template<typename T>
+  expect<T> msgpack::from_bytes(epee::byte_slice&& bytes)
+  {
+    msgpack_reader source{std::move(bytes)};
+    return wire_read::to<T>(source);
+  }
+
+  // specialization prevents type "downgrading" to base type in cpp files
+
+  template<typename T>
+  inline void array(msgpack_reader& source, T& dest)
+  {
+    wire_read::array(source, dest);
+  }
+
+  template<typename... T>
+  inline void object(msgpack_reader& source, T... fields)
+  {
+    wire_read::object(source, wire_read::tracker<T>{std::move(fields)}...);
+  }
+} // wire
diff --git a/src/wire/msgpack/write.cpp b/src/wire/msgpack/write.cpp
new file mode 100644
index 0000000..17197b4
--- /dev/null
+++ b/src/wire/msgpack/write.cpp
@@ -0,0 +1,219 @@
+// Copyright (c) 2023, The Monero Project
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without modification, are
+// permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this list of
+//    conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice, this list
+//    of conditions and the following disclaimer in the documentation and/or other
+//    materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its contributors may be
+//    used to endorse or promote products derived from this software without specific
+//    prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
+// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
+// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
+// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "write.h"
+
+#include <boost/endian/buffers.hpp>
+#include <boost/fusion/include/any.hpp>
+#include <boost/fusion/include/std_tuple.hpp>
+#include <cassert>
+#include <limits>
+#include <ostream>
+#include <stdexcept>
+#include <type_traits>
+
+#include "wire/error.h"
+#include "wire/msgpack/error.h"
+
+namespace
+{
+  template<typename T>
+  using limits = std::numeric_limits<T>;
+
+  constexpr const unsigned flush_threshold = 100;
+  constexpr const unsigned max_buffer = 4096;
+
+  void write_tag(epee::byte_stream& bytes, const wire::msgpack::tag value)
+  {
+    bytes.put(std::uint8_t(value));
+  }
+
+  template<typename T, typename U, wire::msgpack::tag tag>
+  void write_endian(epee::byte_stream& bytes, const T value, const wire::msgpack::type<U, tag> type)
+  {
+    static_assert(std::is_integral<T>::value, "input not integral");
+    static_assert(std::is_integral<U>::value, "output not integral");
+
+    using in_limits = std::numeric_limits<T>;
+    using out_limits = std::numeric_limits<U>;
+    static_assert(in_limits::is_signed == out_limits::is_signed, "signs must match");
+
+    assert(type.min() <= value);
+    assert(value <= type.max());
+
+    static constexpr const std::size_t bits = 8 * sizeof(U);
+    using buffer_type =
+      boost::endian::endian_buffer<boost::endian::order::big, U, bits>;
+
+    buffer_type buffer(value);
+    write_tag(bytes, type.Tag());
+    bytes.write(buffer.data(), sizeof(buffer));
+  }
+
+  template<typename T>
+  void write_count(epee::byte_stream& bytes, const std::uintmax_t items)
+  {
+    const auto match_size = [&bytes, items] (const auto type) -> bool
+    {
+      if (type.max() < items)
+        return false;
+      write_endian(bytes, items, type);
+      return true;
+    };
+    if (!boost::fusion::any(T{}, match_size))
+      WIRE_DLOG_THROW_(wire::error::msgpack::integer_encoding);
+  }
+
+  template<typename T, typename U>
+  void write_count(epee::byte_stream& bytes, const std::uintmax_t items)
+  {
+    if (items <= T::max())
+      bytes.put(T::tag() | std::uint8_t(items));
+    else
+      write_count<U>(bytes, items);
+  }
+}
+
+namespace wire
+{
+  void msgpack_writer::do_flush(epee::span<const std::uint8_t>)
+  {}
+
+  void msgpack_writer::check_flush()
+  {
+    if (needs_flush_ && (max_buffer < bytes_.size() || bytes_.available() < flush_threshold))
+      flush();
+  }
+
+  void msgpack_writer::do_integer(const std::intmax_t value)
+  {
+    assert(value < 0); // constraint checked in header
+    if (0xe0 < value) // 0xe0 needs to be type `int` to work
+    {
+      bytes_.put(std::uint8_t(value));
+      return;
+    }
+
+    const auto match_size = [this, value] (const auto type) -> bool
+    {
+      if (value < type.min())
+        return false;
+      write_endian(bytes_, value, type);
+      return true;
+    };
+    if (!boost::fusion::any(wire::msgpack::signed_types{}, match_size))
+      WIRE_DLOG_THROW_(wire::error::msgpack::integer_encoding);
+  }
+
+  void msgpack_writer::do_unsigned_integer(const std::uintmax_t value)
+  {
+    const auto match_size = [this, value] (const auto type) -> bool
+    {
+      if (type.max() < value)
+        return false;
+      write_endian(bytes_, value, type);
+      return true;
+    };
+    if (!boost::fusion::any(wire::msgpack::unsigned_types{}, match_size))
+      WIRE_DLOG_THROW_(wire::error::msgpack::integer_encoding);
+  }
+
+  void msgpack_writer::check_complete()
+  {
+    if (expected_)
+      throw std::logic_error{"msgpack_writer::take_msgpack() failed with incomplete tree"};
+  }
+  epee::byte_slice msgpack_writer::take_msgpack()
+  {
+    check_complete();
+    epee::byte_slice out{std::move(bytes_)};
+    bytes_.clear();
+    return out;
+  }
+
+  msgpack_writer::~msgpack_writer() noexcept
+  {}
+
+  void msgpack_writer::real(const double source)
+  {
+    write_tag(bytes_, msgpack::tag::float64);
+    bytes_.write(reinterpret_cast<const char*>(std::addressof(source)), sizeof(source));
+    --expected_;
+  }
+
+  void msgpack_writer::string(const boost::string_ref source)
+  {
+    write_count<msgpack::ftag_string, msgpack::string_types>(bytes_, source.size());
+    bytes_.write(source.data(), source.size());
+    --expected_;
+  }
+  void msgpack_writer::binary(epee::span<const std::uint8_t> source)
+  {
+    write_count<msgpack::binary_types>(bytes_, source.size());
+    bytes_.write(source);
+    --expected_;
+  }
+
+  void msgpack_writer::enumeration(const std::size_t index, const epee::span<char const* const> enums)
+  {
+    if (enums.size() < index)
+      throw std::logic_error{"Invalid enum/string value"};
+    unsigned_integer(index);
+  }
+
+  void msgpack_writer::start_array(const std::size_t items)
+  {
+    write_count<msgpack::ftag_array, msgpack::array_types>(bytes_, items);
+    expected_ += items;
+  }
+
+  void msgpack_writer::start_object(const std::size_t items)
+  {
+    write_count<msgpack::ftag_object, msgpack::object_types>(bytes_, items);
+    expected_ += items * 2;
+  }
+  void msgpack_writer::key(const boost::string_ref str)
+  {
+    string(str);
+  }
+  void msgpack_writer::key(const std::uintmax_t id)
+  {
+    unsigned_integer(id);
+  }
+  void msgpack_writer::key(const unsigned id, boost::string_ref str)
+  {
+    if (integer_keys_)
+      key(id);
+    else
+      key(str);
+  }
+
+  void msgpack_stream_writer::do_flush(epee::span<const std::uint8_t> bytes)
+  {
+    dest.write(reinterpret_cast<const char*>(bytes.data()), bytes.size());
+  }
+}
diff --git a/src/wire/msgpack/write.h b/src/wire/msgpack/write.h
new file mode 100644
index 0000000..2b755de
--- /dev/null
+++ b/src/wire/msgpack/write.h
@@ -0,0 +1,225 @@
+// Copyright (c) 2023, The Monero Project
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without modification, are
+// permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this list of
+//    conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice, this list
+//    of conditions and the following disclaimer in the documentation and/or other
+//    materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its contributors may be
+//    used to endorse or promote products derived from this software without specific
+//    prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
+// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
+// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
+// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#pragma once
+
+#include <array>
+#include <boost/utility/string_ref.hpp>
+#include <cstdint>
+#include <iosfwd>
+#include <limits>
+
+#include "byte_stream.h" // monero/contrib/epee/include
+#include "span.h"        // monero/contrib/epee/include
+#include "wire/field.h"
+#include "wire/filters.h"
+#include "wire/msgpack/base.h"
+#include "wire/traits.h"
+#include "wire/write.h"
+
+namespace wire
+{
+  //! Writes MSGPACK tokens one-at-a-time for DOMless output.
+  class msgpack_writer : public writer
+  {
+    epee::byte_stream bytes_;
+    std::size_t expected_; //! Tracks number of expected elements remaining
+    const bool integer_keys_;
+    bool needs_flush_;
+
+    //! Provided data currently in `bytes_`.
+    virtual void do_flush(epee::span<const uint8_t>);
+
+    //! Flush written bytes to `do_flush(...)` if configured
+    void check_flush();
+    
+    void do_integer(std::intmax_t);
+    void do_unsigned_integer(std::uintmax_t);
+    
+    template<typename T>
+    void integer_t(const T value)
+    {
+      if (0 <= value)
+      {
+        if (value <= msgpack::ftag_unsigned::max())
+          bytes_.put(std::uint8_t(value));
+        else // if multibyte
+          do_unsigned_integer(std::uintmax_t(value));
+      }
+      else // if negative
+        do_integer(value);
+      --expected_;
+    }
+
+    template<typename T>
+    void unsigned_integer_t(const T value)
+    {
+      if (value <= msgpack::ftag_unsigned::max())
+        bytes_.put(std::uint8_t(value));
+      else // if multibyte
+        do_unsigned_integer(value);
+      --expected_;
+    }
+
+  protected:
+    msgpack_writer(bool integer_keys, bool needs_flush)
+      : writer(), bytes_(), expected_(1), integer_keys_(integer_keys), needs_flush_(needs_flush)
+    {}
+
+    //! \throw std::logic_error if tree was not completed
+    void check_complete();
+
+    //! \throw std::logic_error if incomplete msgpack tree. \return msgpack bytes
+    epee::byte_slice take_msgpack();
+
+    //! Flush bytes in local buffer to `do_flush(...)`
+    void flush()
+    {
+      do_flush({bytes_.data(), bytes_.size()});
+      bytes_.clear();
+    }
+
+  public:
+    msgpack_writer(const msgpack_writer&) = delete;
+    virtual ~msgpack_writer() noexcept;
+    msgpack_writer& operator=(const msgpack_writer&) = delete;
+
+    void boolean(const bool value) override final
+    {
+      if (value)
+        bytes_.put(std::uint8_t(msgpack::tag::True));
+      else
+        bytes_.put(std::uint8_t(msgpack::tag::False));
+      --expected_;
+    }
+
+    void integer(const int value) override final
+    { integer_t(value); }
+
+    void integer(const std::intmax_t value) override final
+    { integer_t(value); }
+
+    void unsigned_integer(const unsigned value) override final
+    { unsigned_integer_t(value); }
+
+    void unsigned_integer(const std::uintmax_t value) override final
+    { unsigned_integer_t(value); }
+
+    void real(double) override final;
+
+    //! \throw wire::exception if `source.size()` exceeds 2^32-1
+    void string(boost::string_ref source) override final;
+    //! \throw wire::exception if `source.size()` exceeds 2^32-1
+    void binary(epee::span<const std::uint8_t> source) override final;
+
+    void enumeration(std::size_t index, epee::span<char const* const> enums) override final;
+
+    //! \throw wire::exception if `items` exceeds 2^32-1.
+    void start_array(std::size_t items) override final;
+    void end_array() override final { --expected_; }
+
+    //! \throw wire::exception if `items` exceeds 2^32-1
+    void start_object(std::size_t items) override final;
+    void key(std::uintmax_t) override final;
+    void key(boost::string_ref) override final;
+    void key(unsigned, boost::string_ref) override final;
+    void end_object() override final { --expected_; }
+  };
+
+  //! Buffers entire JSON message in memory
+  struct msgpack_slice_writer final : msgpack_writer
+  {
+    explicit msgpack_slice_writer(bool integer_keys = false)
+      : msgpack_writer(integer_keys, false)
+    {}
+
+    //! \throw std::logic_error if incomplete JSON tree \return JSON bytes
+    epee::byte_slice take_bytes()
+    {
+      return msgpack_writer::take_msgpack();
+    }
+  };
+
+  //! Periodically flushes JSON data to `std::ostream`
+  class msgpack_stream_writer final : public msgpack_writer
+  {
+    std::ostream& dest;
+
+    virtual void do_flush(epee::span<const std::uint8_t>) override final;
+  public:
+    explicit msgpack_stream_writer(std::ostream& dest, bool integer_keys = false)
+      : msgpack_writer(integer_keys, true), dest(dest)
+    {}
+
+    //! Flush remaining bytes to stream \throw std::logic_error if incomplete JSON tree
+    void finish()
+    {
+      check_complete();
+      flush();
+    }
+  };
+
+  template<typename T>
+  epee::byte_slice msgpack::to_bytes(const T& source)
+  {
+    return wire_write::to_bytes<msgpack_slice_writer>(source);
+  }
+
+  template<typename T, typename F = identity_>
+  inline void array(msgpack_writer& dest, const T& source, F filter = F{})
+  {
+    wire_write::array(dest, source, source.size(), std::move(filter));
+  }
+  template<typename T, typename F>
+  inline void write_bytes(msgpack_writer& dest, as_array_<T, F> source)
+  {
+    wire::array(dest, source.get_value(), std::move(source.filter));
+  }
+  template<typename T>
+  inline enable_if<is_array<T>::value> write_bytes(msgpack_writer& dest, const T& source)
+  {
+    wire::array(dest, source);
+  }
+
+  template<typename T, typename F = identity_, typename G = identity_>
+  inline void dynamic_object(msgpack_writer& dest, const T& source, F key_filter = F{}, G value_filter = G{})
+  {
+    // works with "lazily" computed ranges
+    wire_write::dynamic_object(dest, source, 0, std::move(key_filter), std::move(value_filter));
+  }
+  template<typename T, typename F, typename G>
+  inline void write_bytes(msgpack_writer& dest, as_object_<T, F, G> source)
+  {
+    wire::dynamic_object(dest, source.get_map(), std::move(source.key_filter), std::move(source.value_filter));
+  }
+
+  template<typename... T>
+  inline void object(msgpack_writer& dest, T... fields)
+  {
+    wire_write::object(dest, std::move(fields)...);
+  }
+}
diff --git a/src/wire/read.h b/src/wire/read.h
index a73560d..e57014f 100644
--- a/src/wire/read.h
+++ b/src/wire/read.h
@@ -1,4 +1,4 @@
-// Copyright (c) 2020, The Monero Project
+// Copyright (c) 2020-2023, The Monero Project
 // All rights reserved.
 //
 // Redistribution and use in source and binary forms, with or without modification, are
@@ -299,14 +299,14 @@ namespace wire_read
     unpack_variant_field(index, source, dest.get_value(), static_cast< const wire::option<U>& >(dest)...);
   }
 
-  template<typename R, typename T>
-  inline void unpack_field(std::size_t, R& source, wire::field_<T, true>& dest)
+  template<typename R, typename T, unsigned I>
+  inline void unpack_field(std::size_t, R& source, wire::field_<T, true, I>& dest)
   {
     read_bytes(source, dest.get_value());
   }
 
-  template<typename R, typename T>
-  inline void unpack_field(std::size_t, R& source, wire::field_<T, false>& dest)
+  template<typename R, typename T, unsigned I>
+  inline void unpack_field(std::size_t, R& source, wire::field_<T, false, I>& dest)
   {
     dest.get_value().emplace();
     read_bytes(source, *dest.get_value());
@@ -322,7 +322,7 @@ namespace wire_read
   inline void expand_field_map(std::size_t index, wire::reader::key_map (&map)[N], const T& head, const U&... tail)
   {
     map[index].name = head.name;
-    map[index].id = 0;
+    map[index].id = head.id();
     expand_field_map(index + 1, map, tail...);
   }
 
diff --git a/src/wire/write.h b/src/wire/write.h
index c89afaa..ca1bbed 100644
--- a/src/wire/write.h
+++ b/src/wire/write.h
@@ -1,4 +1,4 @@
-// Copyright (c) 2020, The Monero Project
+// Copyright (c) 2020-2023, The Monero Project
 // All rights reserved.
 //
 // Redistribution and use in source and binary forms, with or without modification, are
@@ -142,13 +142,18 @@ namespace wire_write
       declared after these functions. */
 
   template<typename W, typename T>
-  inline epee::byte_slice to_bytes(const T& value)
+  inline epee::byte_slice to_bytes(W&& dest, const T& value)
   {
-    W dest{};
     write_bytes(dest, value);
     return dest.take_bytes();
   }
 
+  template<typename W, typename T>
+  inline epee::byte_slice to_bytes(const T& value)
+  {
+    return wire_write::to_bytes(W{}, value);
+  }
+
   template<typename W, typename T, typename F = wire::identity_>
   inline void array(W& dest, const T& source, const std::size_t count, F filter = F{})
   {
@@ -162,20 +167,20 @@ namespace wire_write
     dest.end_array();
   }
 
-  template<typename W, typename T>
-  inline bool field(W& dest, const wire::field_<T, true> elem)
+  template<typename W, typename T, unsigned I>
+  inline bool field(W& dest, const wire::field_<T, true, I> elem)
   {
-    dest.key(0, elem.name);
+    dest.key(I, elem.name);
     write_bytes(dest, elem.get_value());
     return true;
   }
 
-  template<typename W, typename T>
-  inline bool field(W& dest, const wire::field_<T, false> elem)
+  template<typename W, typename T, unsigned I>
+  inline bool field(W& dest, const wire::field_<T, false, I> elem)
   {
     if (bool(elem.get_value()))
     {
-      dest.key(0, elem.name);
+      dest.key(I, elem.name);
       write_bytes(dest, *elem.get_value());
     }
     return true;
diff --git a/tests/unit/CMakeLists.txt b/tests/unit/CMakeLists.txt
index 3412c12..b5b5997 100644
--- a/tests/unit/CMakeLists.txt
+++ b/tests/unit/CMakeLists.txt
@@ -33,5 +33,5 @@ target_link_libraries(monero-lws-unit-framework)
 add_subdirectory(wire)
 
 add_executable(monero-lws-unit main.cpp)
-target_link_libraries(monero-lws-unit monero-lws-unit-framework monero-lws-unit-wire monero-lws-unit-wire-json)
+target_link_libraries(monero-lws-unit monero-lws-unit-framework monero-lws-unit-wire monero-lws-unit-wire-json monero-lws-unit-wire-msgpack)
 add_test(NAME monero-lws-unit COMMAND monero-lws-unit -v)
diff --git a/tests/unit/wire/CMakeLists.txt b/tests/unit/wire/CMakeLists.txt
index f04d781..9d2b11c 100644
--- a/tests/unit/wire/CMakeLists.txt
+++ b/tests/unit/wire/CMakeLists.txt
@@ -1,4 +1,4 @@
-# Copyright (c) 2022, The Monero Project
+# Copyright (c) 2022-2023, The Monero Project
 #
 # All rights reserved.
 #
@@ -28,6 +28,7 @@
 
 
 add_subdirectory(json)
+add_subdirectory(msgpack)
 
 add_library(monero-lws-unit-wire OBJECT read.write.test.cpp read.test.cpp)
 target_link_libraries(
diff --git a/tests/unit/wire/msgpack/CMakeLists.txt b/tests/unit/wire/msgpack/CMakeLists.txt
new file mode 100644
index 0000000..90066e4
--- /dev/null
+++ b/tests/unit/wire/msgpack/CMakeLists.txt
@@ -0,0 +1,37 @@
+# Copyright (c) 2023, The Monero Project
+#
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without modification, are
+# permitted provided that the following conditions are met:
+#
+# 1. Redistributions of source code must retain the above copyright notice, this list of
+#    conditions and the following disclaimer.
+#
+# 2. Redistributions in binary form must reproduce the above copyright notice, this list
+#    of conditions and the following disclaimer in the documentation and/or other
+#    materials provided with the distribution.
+#
+# 3. Neither the name of the copyright holder nor the names of its contributors may be
+#    used to endorse or promote products derived from this software without specific
+#    prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
+# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+# MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
+# THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+# STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
+# THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+add_library(monero-lws-unit-wire-msgpack OBJECT read.write.test.cpp)
+target_link_libraries(
+  monero-lws-unit-wire-msgpack
+  monero-lws-unit-framework
+  monero-lws-wire-msgpack
+  monero::libraries
+)
+
diff --git a/tests/unit/wire/msgpack/read.write.test.cpp b/tests/unit/wire/msgpack/read.write.test.cpp
new file mode 100644
index 0000000..50bdb2b
--- /dev/null
+++ b/tests/unit/wire/msgpack/read.write.test.cpp
@@ -0,0 +1,183 @@
+// Copyright (c) 2023, The Monero Project
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without modification, are
+// permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this list of
+//    conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice, this list
+//    of conditions and the following disclaimer in the documentation and/or other
+//    materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its contributors may be
+//    used to endorse or promote products derived from this software without specific
+//    prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
+// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
+// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
+// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+#include "framework.test.h"
+
+#include <boost/core/demangle.hpp>
+#include <boost/range/algorithm/equal.hpp>
+#include <cstdint>
+#include <type_traits>
+#include "wire/traits.h"
+#include "wire/msgpack.h"
+#include "wire/vector.h"
+
+#include "wire/base.test.h"
+
+namespace
+{
+  constexpr const char basic_string[] = u8"my_string_data";
+  //constexpr const char basic_[] =
+  //  u8"{\"utf8\":\"my_string_data\",\"vec\":[0,127],\"data\":\"00ff2211\",\"choice\":true}";
+  constexpr const std::uint8_t basic_msgpack[] = {
+    0x84, 0xa4, 0x75, 0x74, 0x66, 0x38, 0xae, 0x6d, 0x79, 0x5f, 0x73, 0x74,
+    0x72, 0x69, 0x6e, 0x67, 0x5f, 0x64, 0x61, 0x74, 0x61, 0xa3, 0x76, 0x65,
+    0x63, 0x92, 0x00, 0x7f, 0xa4, 0x64, 0x61, 0x74, 0x61, 0xc4, 0x04, 0x00,
+    0xff, 0x22, 0x11, 0xa6, 0x63, 0x68, 0x6f, 0x69, 0x63, 0x65, 0xc3
+  };
+  constexpr const std::uint8_t advanced_msgpack[] = {
+    0x84, 0x00, 0xae, 0x6d, 0x79, 0x5f, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67,
+    0x5f, 0x64, 0x61, 0x74, 0x61, 0x01, 0x92, 0x00, 0x7f, 0x02, 0xc4, 0x04,
+    0x00, 0xff, 0x22, 0x11, 0xcc, 0xfe, 0xc3
+  };
+
+  template<typename T>
+  struct basic_object
+  {
+    std::string utf8;
+    std::vector<T> vec;
+    lws_test::small_blob data;
+    bool choice;
+  };
+
+  template<typename F, typename T>
+  void basic_object_map(F& format, T& self)
+  {
+    wire::object(format,
+      WIRE_FIELD_ID(0, utf8),
+      WIRE_FIELD_ID(1, vec),
+      WIRE_FIELD_ID(2, data),
+      WIRE_FIELD_ID(254, choice)
+    );
+  }
+
+  template<typename T>
+  void read_bytes(wire::msgpack_reader& source, basic_object<T>& dest)
+  { basic_object_map(source, dest); }
+
+  template<typename T>
+  void write_bytes(wire::msgpack_writer& dest, const basic_object<T>& source)
+  { basic_object_map(dest, source); }
+
+  template<typename T>
+  void test_basic_reading(lest::env& lest_env)
+  {
+    SETUP("Basic (string keys) with " + boost::core::demangle(typeid(T).name()) + " integers")
+    {
+      const auto result =
+        wire::msgpack::from_bytes<basic_object<T>>(epee::byte_slice{{basic_msgpack}});
+      EXPECT(result);
+      EXPECT(result->utf8 == basic_string);
+      {
+        const std::vector<T> expected{0, 127};
+        EXPECT(result->vec == expected);
+      }
+      EXPECT(result->data == lws_test::blob_test1);
+      EXPECT(result->choice);
+    }
+  }
+
+  template<typename T>
+  void test_advanced_reading(lest::env& lest_env)
+  {
+    SETUP("Advanced (integer keys) with " + boost::core::demangle(typeid(T).name()) + " integers")
+    {
+      const auto result =
+        wire::msgpack::from_bytes<basic_object<T>>(epee::byte_slice{{advanced_msgpack}});
+      EXPECT(result);
+      EXPECT(result->utf8 == basic_string);
+      {
+        const std::vector<T> expected{0, 127};
+        EXPECT(result->vec == expected);
+      }
+      EXPECT(result->data == lws_test::blob_test1);
+      EXPECT(result->choice);
+    }
+  }
+
+  template<typename T>
+  void test_basic_writing(lest::env& lest_env)
+  {
+    SETUP("Basic (string keys) with " + boost::core::demangle(typeid(T).name()) + " integers")
+    {
+      const basic_object<T> val{basic_string, std::vector<T>{0, 127}, lws_test::blob_test1, true};
+      const auto result = wire::msgpack::to_bytes(val);
+      EXPECT(boost::range::equal(result, epee::byte_slice{{basic_msgpack}}));
+    }
+  }
+
+  template<typename T>
+  void test_advanced_writing(lest::env& lest_env)
+  {
+    SETUP("Advanced (integer keys) with " + boost::core::demangle(typeid(T).name()) + " integers")
+    {
+      const basic_object<T> val{basic_string, std::vector<T>{0, 127}, lws_test::blob_test1, true};
+      const auto result = wire_write::to_bytes(wire::msgpack_slice_writer{true}, val);
+      EXPECT(boost::range::equal(result, epee::byte_slice{{advanced_msgpack}}));
+    }
+  }
+}
+
+LWS_CASE("wire::msgpack_reader")
+{
+  test_basic_reading<std::int16_t>(lest_env);
+  test_basic_reading<std::int32_t>(lest_env);
+  test_basic_reading<std::int64_t>(lest_env);
+  test_basic_reading<std::intmax_t>(lest_env);
+  test_basic_reading<std::uint16_t>(lest_env);
+  test_basic_reading<std::uint32_t>(lest_env);
+  test_basic_reading<std::uint64_t>(lest_env);
+  test_basic_reading<std::uintmax_t>(lest_env);
+  
+  test_advanced_reading<std::int16_t>(lest_env);
+  test_advanced_reading<std::int32_t>(lest_env);
+  test_advanced_reading<std::int64_t>(lest_env);
+  test_advanced_reading<std::intmax_t>(lest_env);
+  test_advanced_reading<std::uint16_t>(lest_env);
+  test_advanced_reading<std::uint32_t>(lest_env);
+  test_advanced_reading<std::uint64_t>(lest_env);
+  test_advanced_reading<std::uintmax_t>(lest_env);
+}
+
+LWS_CASE("wire::msgpack_writer")
+{
+  test_basic_writing<std::int16_t>(lest_env);
+  test_basic_writing<std::int32_t>(lest_env);
+  test_basic_writing<std::int64_t>(lest_env);
+  test_basic_writing<std::intmax_t>(lest_env);
+  test_basic_writing<std::uint16_t>(lest_env);
+  test_basic_writing<std::uint32_t>(lest_env);
+  test_basic_writing<std::uint64_t>(lest_env);
+  test_basic_writing<std::uintmax_t>(lest_env);
+  
+  test_advanced_writing<std::int16_t>(lest_env);
+  test_advanced_writing<std::int32_t>(lest_env);
+  test_advanced_writing<std::int64_t>(lest_env);
+  test_advanced_writing<std::intmax_t>(lest_env);
+  test_advanced_writing<std::uint16_t>(lest_env);
+  test_advanced_writing<std::uint32_t>(lest_env);
+  test_advanced_writing<std::uint64_t>(lest_env);
+  test_advanced_writing<std::uintmax_t>(lest_env);
+}
diff --git a/tests/unit/wire/read.write.test.cpp b/tests/unit/wire/read.write.test.cpp
index 598b0bd..d194133 100644
--- a/tests/unit/wire/read.write.test.cpp
+++ b/tests/unit/wire/read.write.test.cpp
@@ -1,4 +1,4 @@
-// Copyright (c) 2022, The Monero Project
+// Copyright (c) 2022-2023, The Monero Project
 // All rights reserved.
 //
 // Redistribution and use in source and binary forms, with or without modification, are
@@ -34,6 +34,7 @@
 #include <vector>
 #include "wire.h"
 #include "wire/json.h"
+#include "wire/msgpack.h"
 #include "wire/vector.h"
 
 #include "wire/base.test.h"
@@ -93,7 +94,7 @@ namespace
   void fill(complex& self)
   {
     self.objects = std::vector<inner>{inner{0, limit<std::uint32_t>::max()}, inner{100, 200}, inner{44444, 83434}};
-    self.ints = std::vector<std::int16_t>{limit<std::int16_t>::min(), limit<std::int16_t>::max(), 0, 31234};
+    self.ints = std::vector<std::int16_t>{limit<std::int16_t>::min(), limit<std::int16_t>::max(), -3, 31234};
     self.uints = std::vector<std::uint64_t>{0, limit<std::uint64_t>::max(), 34234234, 33};
     self.blobs = {lws_test::blob_test1, lws_test::blob_test2, lws_test::blob_test3};
     self.strings = {"string1", "string2", "string3", "string4"};
@@ -113,7 +114,7 @@ namespace
     EXPECT(self.ints.size() == 4);
     EXPECT(self.ints.at(0) == limit<std::int16_t>::min());
     EXPECT(self.ints.at(1) == limit<std::int16_t>::max());
-    EXPECT(self.ints.at(2) == 0);
+    EXPECT(self.ints.at(2) == -3);
     EXPECT(self.ints.at(3) == 31234);
 
     EXPECT(self.uints.size() == 4);
@@ -136,7 +137,7 @@ namespace
     EXPECT(self.choice == true);
   }
 
-  template<typename T>
+  template<typename T, typename U>
   void run_complex(lest::env& lest_env)
   {
     SETUP("Complex test for " + boost::core::demangle(typeid(T).name()))
@@ -148,7 +149,7 @@ namespace
         const expect<epee::byte_slice> bytes = T::to_bytes(base);
         EXPECT(bytes);
 
-        const expect<complex> derived = T::template from_bytes<complex>(std::string{bytes->begin(), bytes->end()});
+        const expect<complex> derived = T::template from_bytes<complex>(U{std::string{bytes->begin(), bytes->end()}});
         EXPECT(derived);
         verify_initial(lest_env, *derived);
       }
@@ -159,7 +160,7 @@ namespace
         const expect<epee::byte_slice> bytes = T::to_bytes(base);
         EXPECT(bytes);
 
-        const expect<complex> derived = T::template from_bytes<complex>(std::string{bytes->begin(), bytes->end()});
+        const expect<complex> derived = T::template from_bytes<complex>(U{std::string{bytes->begin(), bytes->end()}});
         EXPECT(derived);
         verify_filled(lest_env, *derived);
       }
@@ -180,7 +181,7 @@ namespace
   WIRE_DEFINE_OBJECT(big, big_map)
   WIRE_DEFINE_OBJECT(small, small_map)
 
-  template<typename T>
+  template<typename T, typename U>
   expect<small> round_trip(lest::env& lest_env, std::int64_t value)
   {
     expect<small> out = small{0};
@@ -188,43 +189,77 @@ namespace
     {
       const expect<epee::byte_slice> bytes = T::template to_bytes(big{value});
       EXPECT(bytes);
-      out = T::template from_bytes<small>(std::string{bytes->begin(), bytes->end()});
+      out = T::template from_bytes<small>(U{std::string{bytes->begin(), bytes->end()}});
     }
     return out;
   }
 
-  template<typename T>
+  template<typename T, typename U>
   void not_overflow(lest::env& lest_env, std::int64_t value)
   {
-    const expect<small> result = round_trip<T>(lest_env, value);
+    const expect<small> result = round_trip<T, U>(lest_env, value);
     EXPECT(result);
     EXPECT(result->value == value);
   }
 
-  template<typename T>
+  template<typename T, typename U>
   void overflow(lest::env& lest_env, std::int64_t value, const std::error_code error)
   {
-    const expect<small> result = round_trip<T>(lest_env, value);
+    const expect<small> result = round_trip<T, U>(lest_env, value);
     EXPECT(result == error);
   }
 
-  template<typename T>
+  template<typename T, typename U>
   void run_overflow(lest::env& lest_env)
   {
     SETUP("Overflow test for " + boost::core::demangle(typeid(T).name()))
     {
-      not_overflow<T>(lest_env, limit<std::int32_t>::min());
-      not_overflow<T>(lest_env, 0);
-      not_overflow<T>(lest_env, limit<std::int32_t>::max());
+      not_overflow<T, U>(lest_env, limit<std::int32_t>::min());
+      not_overflow<T, U>(lest_env, 0);
+      not_overflow<T, U>(lest_env, limit<std::int32_t>::max());
 
-      overflow<T>(lest_env, std::int64_t(limit<std::int32_t>::min()) - 1, wire::error::schema::larger_integer);
-      overflow<T>(lest_env, std::int64_t(limit<std::int32_t>::max()) + 1, wire::error::schema::smaller_integer);
+      overflow<T, U>(lest_env, std::int64_t(limit<std::int32_t>::min()) - 1, wire::error::schema::larger_integer);
+      overflow<T, U>(lest_env, std::int64_t(limit<std::int32_t>::max()) + 1, wire::error::schema::smaller_integer);
     }
   }
+
+  struct simple { bool choice; };
+  static void read_bytes(wire::reader& source, simple& self)
+  {
+    wire::object(source, WIRE_FIELD(choice));
+  }
+
+  template<typename T, typename U>
+  void run_skip(lest::env& lest_env)
+  {
+    complex base{};
+    verify_initial(lest_env, base);
+    fill(base);
+
+    const expect<epee::byte_slice> bytes = T::to_bytes(base);
+    EXPECT(bytes);
+
+    const expect<simple> derived = T::template from_bytes<simple>(U{std::string{bytes->begin(), bytes->end()}});
+    EXPECT(derived);
+    EXPECT(derived->choice);
+  }
 }
 
-LWS_CASE("wire::reader and wire::writer")
+LWS_CASE("wire::reader and wire::writer complex")
 {
-  run_complex<wire::json>(lest_env);
-  run_overflow<wire::json>(lest_env);
+  run_complex<wire::json, std::string>(lest_env);
+  run_complex<wire::msgpack, epee::byte_slice>(lest_env);
 }
+
+LWS_CASE("wire::reader and wire::writer overflow")
+{
+  run_overflow<wire::json, std::string>(lest_env);
+  run_overflow<wire::msgpack, epee::byte_slice>(lest_env);
+}
+
+LWS_CASE("wire::reader and wire::writer skip")
+{
+  run_skip<wire::json, std::string>(lest_env);
+  run_skip<wire::msgpack, epee::byte_slice>(lest_env);
+}
+