diff --git a/indra/llcommon/CMakeLists.txt b/indra/llcommon/CMakeLists.txt
index ca8b5e946f4ad17cc91e946b100b49344d691878..36b2e09dc58c3d9918edf6110a754de4dbdb2893 100644
--- a/indra/llcommon/CMakeLists.txt
+++ b/indra/llcommon/CMakeLists.txt
@@ -30,6 +30,7 @@ include_directories(
 
 set(llcommon_SOURCE_FILES
     indra_constants.cpp
+    lazyeventapi.cpp
     llallocator.cpp
     llallocator_heap_profile.cpp
     llapp.cpp
@@ -128,10 +129,12 @@ set(llcommon_SOURCE_FILES
 set(llcommon_HEADER_FILES
     CMakeLists.txt
 
+    apply.h
     chrono.h
     ctype_workaround.h
     fix_macros.h
     indra_constants.h
+    lazyeventapi.h
     linden_common.h
     llalignedarray.h
     llallocator.h
@@ -338,6 +341,7 @@ if (LL_TESTS)
       ${BOOST_SYSTEM_LIBRARY})
   LL_ADD_INTEGRATION_TEST(commonmisc "" "${test_libs}")
   LL_ADD_INTEGRATION_TEST(bitpack "" "${test_libs}")
+  LL_ADD_INTEGRATION_TEST(lazyeventapi "" "${test_libs}")
   LL_ADD_INTEGRATION_TEST(llbase64 "" "${test_libs}")
   LL_ADD_INTEGRATION_TEST(llcond "" "${test_libs}")
   LL_ADD_INTEGRATION_TEST(lldate "" "${test_libs}")
diff --git a/indra/llcommon/lazyeventapi.cpp b/indra/llcommon/lazyeventapi.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..aefc2db6daf28b56b8e2cae6a415988b4088ed04
--- /dev/null
+++ b/indra/llcommon/lazyeventapi.cpp
@@ -0,0 +1,53 @@
+/**
+ * @file   lazyeventapi.cpp
+ * @author Nat Goodspeed
+ * @date   2022-06-17
+ * @brief  Implementation for lazyeventapi.
+ * 
+ * $LicenseInfo:firstyear=2022&license=viewerlgpl$
+ * Copyright (c) 2022, Linden Research, Inc.
+ * $/LicenseInfo$
+ */
+
+// Precompiled header
+#include "linden_common.h"
+// associated header
+#include "lazyeventapi.h"
+// STL headers
+// std headers
+// external library headers
+// other Linden headers
+#include "llevents.h"
+
+LL::LazyEventAPIBase::LazyEventAPIBase(
+    const std::string& name, const std::string& desc, const std::string& field)
+{
+    // populate embedded LazyEventAPIParams instance
+    mParams.name = name;
+    mParams.desc = desc;
+    mParams.field = field;
+    // mParams.init and mOperations are populated by subsequent add() calls.
+
+    // Our raison d'etre: register as an LLEventPumps::PumpFactory
+    // so obtain() will notice any request for this name and call us.
+    // Of course, our subclass constructor must finish running (making add()
+    // calls) before mParams will be fully populated, but we expect that to
+    // happen well before the first LLEventPumps::obtain(name) call.
+    mRegistered = LLEventPumps::instance().registerPumpFactory(
+        name,
+        [this](const std::string& name){ return construct(name); });
+}
+
+LL::LazyEventAPIBase::~LazyEventAPIBase()
+{
+    // If our constructor's registerPumpFactory() call was unsuccessful, that
+    // probably means somebody else claimed the name first. If that's the
+    // case, do NOT unregister their name out from under them!
+    // If this is a static instance being destroyed at process shutdown,
+    // LLEventPumps will probably have been cleaned up already.
+    if (mRegistered && ! LLEventPumps::wasDeleted())
+    {
+        // unregister the callback to this doomed instance
+        LLEventPumps::instance().unregisterPumpFactory(mParams.name);
+    }
+}
diff --git a/indra/llcommon/lazyeventapi.h b/indra/llcommon/lazyeventapi.h
new file mode 100644
index 0000000000000000000000000000000000000000..2e947899dc2e04fbf660d88e22a267f79b7dec44
--- /dev/null
+++ b/indra/llcommon/lazyeventapi.h
@@ -0,0 +1,204 @@
+/**
+ * @file   lazyeventapi.h
+ * @author Nat Goodspeed
+ * @date   2022-06-16
+ * @brief  Declaring a static module-scope LazyEventAPI registers a specific
+ *         LLEventAPI for future on-demand instantiation.
+ * 
+ * $LicenseInfo:firstyear=2022&license=viewerlgpl$
+ * Copyright (c) 2022, Linden Research, Inc.
+ * $/LicenseInfo$
+ */
+
+#if ! defined(LL_LAZYEVENTAPI_H)
+#define LL_LAZYEVENTAPI_H
+
+#include "apply.h"
+#include "lleventapi.h"
+#include "llinstancetracker.h"
+#include <boost/signals2/signal.hpp>
+#include <string>
+#include <tuple>
+#include <utility>                  // std::pair
+#include <vector>
+
+namespace LL
+{
+    /**
+     * Bundle params we want to pass to LLEventAPI's protected constructor. We
+     * package them this way so a subclass constructor can simply forward an
+     * opaque reference to the LLEventAPI constructor.
+     */
+    // This is a class instead of a plain struct mostly so when we forward-
+    // declare it we don't have to remember the distinction.
+    class LazyEventAPIParams
+    {
+    public:
+        // package the parameters used by the normal LLEventAPI constructor
+        std::string name, desc, field;
+        // bundle LLEventAPI::add() calls collected by LazyEventAPI::add(), so
+        // the special LLEventAPI constructor we engage can "play back" those
+        // add() calls
+        boost::signals2::signal<void(LLEventAPI*)> init;
+    };
+
+    // The tricky part is: can we capture a sequence of add() calls in the
+    // LazyEventAPI subclass constructor and then, in effect, replay those
+    // add() calls on instantiation of the registered LLEventAPI subclass? so
+    // we don't have to duplicate the add() calls in both constructors?
+
+    // Derive a subclass from LazyEventAPI. Its constructor must pass
+    // LazyEventAPI's constructor the name, desc, field params. Moreover the
+    // constructor body must call add(name, desc, *args) for any of the
+    // LLEventDispatcher add() methods, referencing the LLEventAPI subclass
+    // methods.
+
+    // LazyEventAPI will store the name, desc, field params for the overall
+    // LLEventAPI. It will support a single generic add() call accepting name,
+    // desc, parameter pack.
+
+    // It will hold a std::vector<std::pair<name, desc>> for each operation.
+    // It will make all these strings available to LLLeapListener.
+
+    // Maybe what we want is to store a vector of callables (a
+    // boost::signals2!) and populate it with lambdas, each of which accepts
+    // LLEventAPI* and calls the relevant add() method by forwarding exactly
+    // the name, desc and parameter pack. Then, on constructing the target
+    // LLEventAPI, we just fire the signal, passing the new instance pointer.
+
+    /**
+     * LazyEventAPIBase implements most of the functionality of LazyEventAPI
+     * (q.v.), but we need the LazyEventAPI template subclass so we can accept
+     * the specific LLEventAPI subclass type.
+     */
+    // No LLInstanceTracker key: we don't need to find a specific instance,
+    // LLLeapListener just needs to be able to enumerate all instances.
+    class LazyEventAPIBase: public LLInstanceTracker<LazyEventAPIBase>
+    {
+    public:
+        LazyEventAPIBase(const std::string& name, const std::string& desc,
+                         const std::string& field);
+        virtual ~LazyEventAPIBase();
+
+        // Do not copy or move: once constructed, LazyEventAPIBase must stay
+        // put: we bind its instance pointer into a callback.
+        LazyEventAPIBase(const LazyEventAPIBase&) = delete;
+        LazyEventAPIBase(LazyEventAPIBase&&) = delete;
+        LazyEventAPIBase& operator=(const LazyEventAPIBase&) = delete;
+        LazyEventAPIBase& operator=(LazyEventAPIBase&&) = delete;
+
+        // actually instantiate the companion LLEventAPI subclass
+        virtual LLEventPump* construct(const std::string& name) = 0;
+
+        // capture add() calls we want to play back on LLEventAPI construction
+        template <typename... ARGS>
+        void add(const std::string& name, const std::string& desc, ARGS&&... rest)
+        {
+            // capture the metadata separately
+            mOperations.push_back(std::make_pair(name, desc));
+            // Use connect_extended() so the lambda is passed its own
+            // connection.
+            // We can't bind an unexpanded parameter pack into a lambda --
+            // shame really. Instead, capture it as a std::tuple and then, in
+            // the lambda, use apply() to convert back to function args.
+            mParams.init.connect_extended(
+                [name, desc, rest = std::make_tuple(std::forward<ARGS>(rest)...)]
+                (const boost::signals2::connection& conn, LLEventAPI* instance)
+                {
+                    // we only need this connection once
+                    conn.disconnect();
+                    // Our add() method distinguishes name and desc because we
+                    // capture them separately. But now, because apply()
+                    // expects a tuple specifying ALL the arguments, expand to
+                    // a tuple including add_trampoline() arguments: instance,
+                    // name, desc, rest.
+                    // apply() can't accept a template per se; it needs a
+                    // particular specialization.
+                    apply(&LazyEventAPIBase::add_trampoline<const std::string&, const std::string&,  ARGS...>,
+                          std::tuple_cat(std::make_tuple(instance, name, desc),
+                                         rest));
+                });
+        }
+
+        // metadata that might be queried by LLLeapListener
+        std::vector<std::pair<std::string, std::string>> mOperations;
+        // Params with which to instantiate the companion LLEventAPI subclass
+        LazyEventAPIParams mParams;
+
+    private:
+        // Passing an overloaded function to any function that accepts an
+        // arbitrary callable is a PITB because you have to specify the
+        // correct overload. What we want is for the compiler to select the
+        // correct overload, based on the carefully-wrought enable_ifs in
+        // LLEventDispatcher. This (one and only) add_trampoline() method
+        // exists solely to pass to LL::apply(). Once add_trampoline() is
+        // called with the expanded arguments, we hope the compiler will Do
+        // The Right Thing in selecting the correct LLEventAPI::add()
+        // overload.
+        template <typename... ARGS>
+        static
+        void add_trampoline(LLEventAPI* instance, ARGS&&... args)
+        {
+            instance->add(std::forward<ARGS>(args)...);
+        }
+
+        bool mRegistered;
+    };
+
+    /**
+     * LazyEventAPI provides a way to register a particular LLEventAPI to be
+     * instantiated on demand, that is, when its name is passed to
+     * LLEventPumps::obtain().
+     *
+     * Derive your listener from LLEventAPI as usual, with its various
+     * operation methods, but code your constructor to accept
+     * <tt>(const LL::LazyEventAPIParams& params)</tt>
+     * and forward that reference to (the protected)
+     * <tt>LLEventAPI(const LL::LazyEventAPIParams&)</tt> constructor.
+     *
+     * Then derive your listener registrar from
+     * <tt>LazyEventAPI<your LLEventAPI subclass></tt>. The constructor should
+     * look very like a traditional LLEventAPI constructor:
+     *
+     * * pass (name, desc [, field]) to LazyEventAPI's constructor
+     * * in the body, make a series of add() calls referencing your LLEventAPI
+     *   subclass methods.
+     *
+     * You may use any LLEventAPI::add() methods, that is, any
+     * LLEventDispatcher::add() methods. But the target methods you pass to
+     * add() must belong to your LLEventAPI subclass, not the LazyEventAPI
+     * subclass.
+     *
+     * Declare a static instance of your LazyEventAPI listener registrar
+     * class. When it's constructed at static initialization time, it will
+     * register your LLEventAPI subclass with LLEventPumps. It will also
+     * collect metadata for the LLEventAPI and its operations to provide to
+     * LLLeapListener's introspection queries.
+     *
+     * When someone later calls LLEventPumps::obtain() to post an event to
+     * your LLEventAPI subclass, obtain() will instantiate it using
+     * LazyEventAPI's name, desc, field and add() calls.
+     */
+    template <class EVENTAPI>
+    class LazyEventAPI: public LazyEventAPIBase
+    {
+    public:
+        // for subclass constructor to reference handler methods
+        using listener = EVENTAPI;
+
+        LazyEventAPI(const std::string& name, const std::string& desc,
+                     const std::string& field="op"):
+            // Forward ctor params to LazyEventAPIBase
+            LazyEventAPIBase(name, desc, field)
+        {}
+
+        LLEventPump* construct(const std::string& /*name*/) override
+        {
+            // base class has carefully assembled LazyEventAPIParams embedded
+            // in this instance, just pass to LLEventAPI subclass constructor
+            return new EVENTAPI(mParams);
+        }
+    };
+} // namespace LL
+
+#endif /* ! defined(LL_LAZYEVENTAPI_H) */
diff --git a/indra/llcommon/lleventapi.cpp b/indra/llcommon/lleventapi.cpp
index ff5459c1eb843c4476979aeeedd420a5080ee7cd..3d46ef1034753ebf3f60518be561a5858545397a 100644
--- a/indra/llcommon/lleventapi.cpp
+++ b/indra/llcommon/lleventapi.cpp
@@ -35,6 +35,7 @@
 // external library headers
 // other Linden headers
 #include "llerror.h"
+#include "lazyeventapi.h"
 
 LLEventAPI::LLEventAPI(const std::string& name, const std::string& desc, const std::string& field):
     lbase(name, field),
@@ -43,6 +44,13 @@ LLEventAPI::LLEventAPI(const std::string& name, const std::string& desc, const s
 {
 }
 
+LLEventAPI::LLEventAPI(const LL::LazyEventAPIParams& params):
+    LLEventAPI(params.name, params.desc, params.field)
+{
+    // call initialization functions with our brand-new instance pointer
+    params.init(this);
+}
+
 LLEventAPI::~LLEventAPI()
 {
 }
diff --git a/indra/llcommon/lleventapi.h b/indra/llcommon/lleventapi.h
index ed62fa064a85de877f81436345f7c7c52f8fdaeb..a0194585533c976c9d50831cb002248489aa4ab9 100644
--- a/indra/llcommon/lleventapi.h
+++ b/indra/llcommon/lleventapi.h
@@ -35,6 +35,13 @@
 #include "llinstancetracker.h"
 #include <string>
 
+namespace LL
+{
+    template <class EVENTAPI>
+    class LazyEventAPI;
+    class LazyEventAPIParams;
+}
+
 /**
  * LLEventAPI not only provides operation dispatch functionality, inherited
  * from LLDispatchListener -- it also gives us event API introspection.
@@ -45,6 +52,8 @@ class LL_COMMON_API LLEventAPI: public LLDispatchListener,
 {
     typedef LLDispatchListener lbase;
     typedef LLInstanceTracker<LLEventAPI, std::string> ibase;
+    template <class EVENTAPI>
+    friend class LL::LazyEventAPI;
 
 public:
 
@@ -137,16 +146,20 @@ class LL_COMMON_API LLEventAPI: public LLDispatchListener,
          * @endcode
          */
         LLSD& operator[](const LLSD::String& key) { return mResp[key]; }
-		
-		 /**
-		 * set the response to the given data
-		 */
-		void setResponse(LLSD const & response){ mResp = response; }
+
+         /**
+         * set the response to the given data
+         */
+        void setResponse(LLSD const & response){ mResp = response; }
 
         LLSD mResp, mReq;
         LLSD::String mKey;
     };
 
+protected:
+    // constructor used only by subclasses registered by LazyEventAPI
+    LLEventAPI(const LL::LazyEventAPIParams&);
+
 private:
     std::string mDesc;
 };
diff --git a/indra/llcommon/lleventdispatcher.cpp b/indra/llcommon/lleventdispatcher.cpp
index 742d6cf51f8510877a442b5ffaa8ec0c5b7f2e65..bc53ec3da03c5e3fbaab57c2dd081ff195a75c79 100644
--- a/indra/llcommon/lleventdispatcher.cpp
+++ b/indra/llcommon/lleventdispatcher.cpp
@@ -706,8 +706,17 @@ LLSD LLEventDispatcher::getMetadata(const std::string& name) const
 
 LLDispatchListener::LLDispatchListener(const std::string& pumpname, const std::string& key):
     LLEventDispatcher(pumpname, key),
-    mPump(pumpname, true),          // allow tweaking for uniqueness
-    mBoundListener(mPump.listen("self", boost::bind(&LLDispatchListener::process, this, _1)))
+    // Do NOT tweak the passed pumpname. In practice, when someone
+    // instantiates a subclass of our LLEventAPI subclass, they intend to
+    // claim that LLEventPump name in the global LLEventPumps namespace. It
+    // would be mysterious and distressing if we allowed name tweaking, and
+    // someone else claimed pumpname first for a completely unrelated
+    // LLEventPump. Posted events would never reach our subclass listener
+    // because we would have silently changed its name; meanwhile listeners
+    // (if any) on that other LLEventPump would be confused by the events
+    // intended for our subclass.
+    LLEventStream(pumpname, false),
+    mBoundListener(listen("self", [this](const LLSD& event){ return process(event); }))
 {
 }
 
diff --git a/indra/llcommon/lleventdispatcher.h b/indra/llcommon/lleventdispatcher.h
index 1b3e834aeb0d792d13db8e9ffe8c196f5f5d6323..ce9d3775cc9c07f0f26b25cc1954916d99c4290c 100644
--- a/indra/llcommon/lleventdispatcher.h
+++ b/indra/llcommon/lleventdispatcher.h
@@ -165,12 +165,12 @@ class LL_COMMON_API LLEventDispatcher
      * When calling this name, pass an LLSD::Array. Each entry in turn will be
      * converted to the corresponding parameter type using LLSDParam.
      */
-    template<typename Function>
-    typename std::enable_if<
-        boost::function_types::is_nonmember_callable_builtin<Function>::value
-        >::type add(const std::string& name,
-                    const std::string& desc,
-                    Function f);
+    // enable_if usage per https://stackoverflow.com/a/39913395/5533635
+    template<typename Function,
+             typename = typename std::enable_if<
+                 boost::function_types::is_nonmember_callable_builtin<Function>::value
+             >::type>
+    void add(const std::string& name, const std::string& desc, Function f);
 
     /**
      * Register a nonstatic class method with arbitrary parameters.
@@ -189,14 +189,13 @@ class LL_COMMON_API LLEventDispatcher
      * When calling this name, pass an LLSD::Array. Each entry in turn will be
      * converted to the corresponding parameter type using LLSDParam.
      */
-    template<typename Method, typename InstanceGetter>
-    typename std::enable_if<
-        boost::function_types::is_member_function_pointer<Method>::value &&
-        ! std::is_convertible<InstanceGetter, LLSD>::value
-        >::type add(const std::string& name,
-                    const std::string& desc,
-                    Method f,
-                    const InstanceGetter& getter);
+    template<typename Method, typename InstanceGetter,
+             typename = typename std::enable_if<
+                 boost::function_types::is_member_function_pointer<Method>::value &&
+                 ! std::is_convertible<InstanceGetter, LLSD>::value
+             >::type>
+    void add(const std::string& name, const std::string& desc, Method f,
+             const InstanceGetter& getter);
 
     /**
      * Register a free function with arbitrary parameters. (This also works
@@ -213,14 +212,12 @@ class LL_COMMON_API LLEventDispatcher
      * an LLSD::Array using LLSDArgsMapper and then convert each entry in turn
      * to the corresponding parameter type using LLSDParam.
      */
-    template<typename Function>
-    typename std::enable_if<
-        boost::function_types::is_nonmember_callable_builtin<Function>::value
-        >::type add(const std::string& name,
-                    const std::string& desc,
-                    Function f,
-                    const LLSD& params,
-                    const LLSD& defaults=LLSD());
+    template<typename Function,
+             typename = typename std::enable_if<
+                 boost::function_types::is_nonmember_callable_builtin<Function>::value
+             >::type>
+    void add(const std::string& name, const std::string& desc, Function f,
+             const LLSD& params, const LLSD& defaults=LLSD());
 
     /**
      * Register a nonstatic class method with arbitrary parameters.
@@ -243,16 +240,14 @@ class LL_COMMON_API LLEventDispatcher
      * an LLSD::Array using LLSDArgsMapper and then convert each entry in turn
      * to the corresponding parameter type using LLSDParam.
      */
-    template<typename Method, typename InstanceGetter>
-    typename std::enable_if<
-        boost::function_types::is_member_function_pointer<Method>::value &&
-        ! std::is_convertible<InstanceGetter, LLSD>::value
-        >::type add(const std::string& name,
-                    const std::string& desc,
-                    Method f,
-                    const InstanceGetter& getter,
-                    const LLSD& params,
-                    const LLSD& defaults=LLSD());
+    template<typename Method, typename InstanceGetter,
+             typename = typename std::enable_if<
+                 boost::function_types::is_member_function_pointer<Method>::value &&
+                 ! std::is_convertible<InstanceGetter, LLSD>::value
+             >::type>
+    void add(const std::string& name, const std::string& desc, Method f,
+             const InstanceGetter& getter, const LLSD& params,
+             const LLSD& defaults=LLSD());
 
     //@}    
 
@@ -476,9 +471,8 @@ struct LLEventDispatcher::invoker<Function,To,To>
     }
 };
 
-template<typename Function>
-typename std::enable_if< boost::function_types::is_nonmember_callable_builtin<Function>::value >::type
-LLEventDispatcher::add(const std::string& name, const std::string& desc, Function f)
+template<typename Function, typename>
+void LLEventDispatcher::add(const std::string& name, const std::string& desc, Function f)
 {
     // Construct an invoker_function, a callable accepting const args_source&.
     // Add to DispatchMap an ArrayParamsDispatchEntry that will handle the
@@ -487,13 +481,9 @@ LLEventDispatcher::add(const std::string& name, const std::string& desc, Functio
                                 boost::function_types::function_arity<Function>::value);
 }
 
-template<typename Method, typename InstanceGetter>
-typename std::enable_if<
-    boost::function_types::is_member_function_pointer<Method>::value &&
-    ! std::is_convertible<InstanceGetter, LLSD>::value
->::type
-LLEventDispatcher::add(const std::string& name, const std::string& desc, Method f,
-                       const InstanceGetter& getter)
+template<typename Method, typename InstanceGetter, typename>
+void LLEventDispatcher::add(const std::string& name, const std::string& desc, Method f,
+                            const InstanceGetter& getter)
 {
     // Subtract 1 from the compile-time arity because the getter takes care of
     // the first parameter. We only need (arity - 1) additional arguments.
@@ -501,23 +491,18 @@ LLEventDispatcher::add(const std::string& name, const std::string& desc, Method
                                 boost::function_types::function_arity<Method>::value - 1);
 }
 
-template<typename Function>
-typename std::enable_if< boost::function_types::is_nonmember_callable_builtin<Function>::value >::type
-LLEventDispatcher::add(const std::string& name, const std::string& desc, Function f,
-                       const LLSD& params, const LLSD& defaults)
+template<typename Function, typename>
+void LLEventDispatcher::add(const std::string& name, const std::string& desc, Function f,
+                            const LLSD& params, const LLSD& defaults)
 {
     // See comments for previous is_nonmember_callable_builtin add().
     addMapParamsDispatchEntry(name, desc, make_invoker(f), params, defaults);
 }
 
-template<typename Method, typename InstanceGetter>
-typename std::enable_if<
-    boost::function_types::is_member_function_pointer<Method>::value &&
-    ! std::is_convertible<InstanceGetter, LLSD>::value
->::type
-LLEventDispatcher::add(const std::string& name, const std::string& desc, Method f,
-                       const InstanceGetter& getter,
-                       const LLSD& params, const LLSD& defaults)
+template<typename Method, typename InstanceGetter, typename>
+void LLEventDispatcher::add(const std::string& name, const std::string& desc, Method f,
+                            const InstanceGetter& getter,
+                            const LLSD& params, const LLSD& defaults)
 {
     addMapParamsDispatchEntry(name, desc, make_invoker(f, getter), params, defaults);
 }
@@ -560,17 +545,21 @@ LLEventDispatcher::make_invoker(Method f, const InstanceGetter& getter)
  * LLEventPump name and dispatch key, and add() its methods. Incoming events
  * will automatically be dispatched.
  */
-class LL_COMMON_API LLDispatchListener: public LLEventDispatcher
+// Instead of containing an LLEventStream, LLDispatchListener derives from it.
+// This allows an LLEventPumps::PumpFactory to return a pointer to an
+// LLDispatchListener (subclass) instance, and still have ~LLEventPumps()
+// properly clean it up.
+class LL_COMMON_API LLDispatchListener:
+    public LLEventDispatcher,
+    public LLEventStream
 {
 public:
     LLDispatchListener(const std::string& pumpname, const std::string& key);
-
-    std::string getPumpName() const { return mPump.getName(); }
+    virtual ~LLDispatchListener() {}
 
 private:
     bool process(const LLSD& event);
 
-    LLEventStream mPump;
     LLTempBoundListener mBoundListener;
 };
 
diff --git a/indra/llcommon/llevents.cpp b/indra/llcommon/llevents.cpp
index 5725dad9cc2a2b8493abcb230324c49789dabead..1a305ec3dc453de445f2d00dbf9ac9c1eb5635a9 100644
--- a/indra/llcommon/llevents.cpp
+++ b/indra/llcommon/llevents.cpp
@@ -90,6 +90,13 @@ bool LLEventPumps::registerTypeFactory(const std::string& type, const TypeFactor
     return true;
 }
 
+void LLEventPumps::unregisterTypeFactory(const std::string& type)
+{
+    auto found = mFactories.find(type);
+    if (found != mFactories.end())
+        mFactories.erase(found);
+}
+
 bool LLEventPumps::registerPumpFactory(const std::string& name, const PumpFactory& factory)
 {
     // Do we already have a pump by this name?
@@ -109,10 +116,30 @@ bool LLEventPumps::registerPumpFactory(const std::string& name, const PumpFactor
     static std::string nul(1, '\0');
     std::string type_name{ nul + name };
     mTypes[name] = type_name;
-    mFactories[type_name] = factory;
+    // TypeFactory is called with (name, tweak, type), whereas PumpFactory
+    // accepts only name. We could adapt with std::bind(), but this lambda
+    // does the trick.
+    mFactories[type_name] =
+        [factory]
+        (const std::string& name, bool /*tweak*/, const std::string& /*type*/)
+        { return factory(name); };
     return true;
 }
 
+void LLEventPumps::unregisterPumpFactory(const std::string& name)
+{
+    auto tfound = mTypes.find(name);
+    if (tfound != mTypes.end())
+    {
+        auto ffound = mFactories.find(tfound->second);
+        if (ffound != mFactories.end())
+        {
+            mFactories.erase(ffound);
+        }
+        mTypes.erase(tfound);
+    }
+}
+
 LLEventPump& LLEventPumps::obtain(const std::string& name)
 {
     PumpMap::iterator found = mPumpMap.find(name);
diff --git a/indra/llcommon/llevents.h b/indra/llcommon/llevents.h
index 38adc31121cfa9a8875930fa207d8f0f58613c6d..c1dbf4392f730fa1dc29b8d117bfebb6cd65233f 100644
--- a/indra/llcommon/llevents.h
+++ b/indra/llcommon/llevents.h
@@ -280,6 +280,7 @@ class LL_COMMON_API LLEventPumps: public LLSingleton<LLEventPumps>,
      * a TypeFactory for the specified @a type name.
      */
     bool registerTypeFactory(const std::string& type, const TypeFactory& factory);
+    void unregisterTypeFactory(const std::string& type);
 
     /// function passed to registerPumpFactory()
     typedef std::function<LLEventPump*(const std::string&)> PumpFactory;
@@ -304,6 +305,7 @@ class LL_COMMON_API LLEventPumps: public LLSingleton<LLEventPumps>,
      *   instantiated an LLEventPump(name), so obtain(name) returned that.
      */
     bool registerPumpFactory(const std::string& name, const PumpFactory& factory);
+    void unregisterPumpFactory(const std::string& name);
 
     /**
      * Find the named LLEventPump instance. If it exists post the message to it.
@@ -362,13 +364,13 @@ class LL_COMMON_API LLEventPumps: public LLSingleton<LLEventPumps>,
     typedef std::set<LLEventPump*> PumpSet;
     PumpSet mOurPumps;
     // for make(), map string type name to LLEventPump subclass factory function
-    typedef std::map<std::string, PumpFactory> PumpFactories;
+    typedef std::map<std::string, TypeFactory> TypeFactories;
     // Data used by make().
     // One might think mFactories and mTypes could reasonably be static. So
     // they could -- if not for the fact that make() or obtain() might be
     // called before this module's static variables have been initialized.
     // This is why we use singletons in the first place.
-    PumpFactories mFactories;
+    TypeFactories mFactories;
 
     // for obtain(), map desired string instance name to string type when
     // obtain() must create the instance
diff --git a/indra/llcommon/tests/lazyeventapi_test.cpp b/indra/llcommon/tests/lazyeventapi_test.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..6639c5e54078c2643b004f76ffd48cd160bfc6c9
--- /dev/null
+++ b/indra/llcommon/tests/lazyeventapi_test.cpp
@@ -0,0 +1,89 @@
+/**
+ * @file   lazyeventapi_test.cpp
+ * @author Nat Goodspeed
+ * @date   2022-06-18
+ * @brief  Test for lazyeventapi.
+ * 
+ * $LicenseInfo:firstyear=2022&license=viewerlgpl$
+ * Copyright (c) 2022, Linden Research, Inc.
+ * $/LicenseInfo$
+ */
+
+// Precompiled header
+#include "linden_common.h"
+// associated header
+#include "lazyeventapi.h"
+// STL headers
+// std headers
+// external library headers
+// other Linden headers
+#include "../test/lltut.h"
+#include "llevents.h"
+
+// LLEventAPI listener subclass
+class MyListener: public LLEventAPI
+{
+public:
+    MyListener(const LL::LazyEventAPIParams& params):
+        LLEventAPI(params)
+    {}
+
+    void get(const LLSD& event)
+    {
+        std::cout << "MyListener::get() got " << event << std::endl;
+    }
+};
+
+// LazyEventAPI registrar subclass
+class MyRegistrar: public LL::LazyEventAPI<MyListener>
+{
+    using super = LL::LazyEventAPI<MyListener>;
+    using super::listener;
+public:
+    MyRegistrar():
+        super("Test", "This is a test LLEventAPI")
+    {
+        add("get", "This is a get operation", &listener::get);            
+    }
+};
+// Normally we'd declare a static instance of MyRegistrar -- but because we
+// may want to test with and without, defer declaration to individual test
+// methods.
+
+/*****************************************************************************
+*   TUT
+*****************************************************************************/
+namespace tut
+{
+    struct lazyeventapi_data
+    {
+        ~lazyeventapi_data()
+        {
+            // after every test, reset LLEventPumps
+            LLEventPumps::deleteSingleton();
+        }
+    };
+    typedef test_group<lazyeventapi_data> lazyeventapi_group;
+    typedef lazyeventapi_group::object object;
+    lazyeventapi_group lazyeventapigrp("lazyeventapi");
+
+    template<> template<>
+    void object::test<1>()
+    {
+        set_test_name("LazyEventAPI");
+        // this is where the magic (should) happen
+        // 'register' still a keyword until C++17
+        MyRegistrar regster;
+        LLEventPumps::instance().obtain("Test").post("hey");
+    }
+
+    template<> template<>
+    void object::test<2>()
+    {
+        set_test_name("No LazyEventAPI");
+        // Because the MyRegistrar declaration in test<1>() is local, because
+        // it has been destroyed, we fully expect NOT to reach a MyListener
+        // instance with this post.
+        LLEventPumps::instance().obtain("Test").post("moot");
+    }
+} // namespace tut