diff --git a/indra/llcommon/lazyeventapi.cpp b/indra/llcommon/lazyeventapi.cpp
index aefc2db6daf28b56b8e2cae6a415988b4088ed04..028af9f33f3a9f9dc3619af6bcb667e594e5eb7a 100644
--- a/indra/llcommon/lazyeventapi.cpp
+++ b/indra/llcommon/lazyeventapi.cpp
@@ -15,9 +15,11 @@
 #include "lazyeventapi.h"
 // STL headers
 // std headers
+#include <algorithm>                // std::find_if
 // external library headers
 // other Linden headers
 #include "llevents.h"
+#include "llsdutil.h"
 
 LL::LazyEventAPIBase::LazyEventAPIBase(
     const std::string& name, const std::string& desc, const std::string& field)
@@ -51,3 +53,20 @@ LL::LazyEventAPIBase::~LazyEventAPIBase()
         LLEventPumps::instance().unregisterPumpFactory(mParams.name);
     }
 }
+
+LLSD LL::LazyEventAPIBase::getMetadata(const std::string& name) const
+{
+    // Since mOperations is a vector rather than a map, just search.
+    auto found = std::find_if(mOperations.begin(), mOperations.end(),
+                              [&name](const auto& namedesc)
+                              { return (namedesc.first == name); });
+    if (found == mOperations.end())
+        return {};
+
+    // LLEventDispatcher() supplements the returned metadata in different
+    // ways, depending on metadata provided to the specific add() method.
+    // Don't try to emulate all that. At some point we might consider more
+    // closely unifying LLEventDispatcher machinery with LazyEventAPI, but for
+    // now this will have to do.
+    return llsd::map("name", found->first, "desc", found->second);
+}
diff --git a/indra/llcommon/lazyeventapi.h b/indra/llcommon/lazyeventapi.h
index 7267a3e4ec5361190ad4530285bf14f23cdc7fc7..a815b119f0e6460fc05434faa6aeccab3e76bd82 100644
--- a/indra/llcommon/lazyeventapi.h
+++ b/indra/llcommon/lazyeventapi.h
@@ -63,9 +63,6 @@ namespace LL
         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)
@@ -96,12 +93,40 @@ namespace LL
                 });
         }
 
+        // The following queries mimic the LLEventAPI / LLEventDispatcher
+        // query API.
+
+        // Get the string name of the subject LLEventAPI
+        std::string getName() const { return mParams.name; }
+        // Get the documentation string
+        std::string getDesc() const { return mParams.desc; }
+        // Retrieve the LLSD key we use for dispatching
+        std::string getDispatchKey() const { return mParams.field; }
+
+        // operations
+        using NameDesc = std::pair<std::string, std::string>;
+
+    private:
         // metadata that might be queried by LLLeapListener
-        std::vector<std::pair<std::string, std::string>> mOperations;
+        std::vector<NameDesc> mOperations;
+
+    public:
+        using const_iterator = decltype(mOperations)::const_iterator;
+        const_iterator begin() const { return mOperations.begin(); }
+        const_iterator end()   const { return mOperations.end(); }
+        LLSD getMetadata(const std::string& name) const;
+
+    protected:
         // Params with which to instantiate the companion LLEventAPI subclass
         LazyEventAPIParams mParams;
 
     private:
+        // true if we successfully registered our LLEventAPI on construction
+        bool mRegistered;
+
+        // actually instantiate the companion LLEventAPI subclass
+        virtual LLEventPump* construct(const std::string& name) = 0;
+
         // 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
@@ -117,8 +142,6 @@ namespace LL
         {
             instance->add(std::forward<ARGS>(args)...);
         }
-
-        bool mRegistered;
     };
 
     /**
@@ -168,6 +191,7 @@ namespace LL
             LazyEventAPIBase(name, desc, field)
         {}
 
+    private:
         LLEventPump* construct(const std::string& /*name*/) override
         {
             // base class has carefully assembled LazyEventAPIParams embedded
diff --git a/indra/llcommon/llleaplistener.cpp b/indra/llcommon/llleaplistener.cpp
index 11bfec1b31f71ce7f0993a192a9e1e95d92dfed5..471f52e91c841a18a32b0bb65ea59c1c136ec3c1 100644
--- a/indra/llcommon/llleaplistener.cpp
+++ b/indra/llcommon/llleaplistener.cpp
@@ -14,14 +14,16 @@
 // associated header
 #include "llleaplistener.h"
 // STL headers
-#include <map>
+#include <algorithm>                // std::find_if
 #include <functional>
+#include <map>
+#include <set>
 // std headers
 // external library headers
-#include <boost/foreach.hpp>
 // other Linden headers
-#include "lluuid.h"
+#include "lazyeventapi.h"
 #include "llsdutil.h"
+#include "lluuid.h"
 #include "stringize.h"
 
 /*****************************************************************************
@@ -110,7 +112,7 @@ LLLeapListener::~LLLeapListener()
     // value_type, and Bad Things would happen if you copied an
     // LLTempBoundListener. (Destruction of the original would disconnect the
     // listener, invalidating every stored connection.)
-    BOOST_FOREACH(ListenersMap::value_type& pair, mListeners)
+    for (ListenersMap::value_type& pair : mListeners)
     {
         pair.second.disconnect();
     }
@@ -208,31 +210,65 @@ void LLLeapListener::getAPIs(const LLSD& request) const
 {
     Response reply(LLSD(), request);
 
+    // first, traverse existing LLEventAPI instances
+    std::set<std::string> instances;
     for (auto& ea : LLEventAPI::instance_snapshot())
     {
-        LLSD info;
-        info["desc"] = ea.getDesc();
-        reply[ea.getName()] = info;
+        // remember which APIs are actually instantiated
+        instances.insert(ea.getName());
+        reply[ea.getName()] = llsd::map("desc", ea.getDesc());
+    }
+    // supplement that with *potential* instances: that is, instances of
+    // LazyEventAPI that can each instantiate an LLEventAPI on demand
+    for (const auto& lea : LL::LazyEventAPIBase::instance_snapshot())
+    {
+        // skip any LazyEventAPI that's already instantiated its LLEventAPI
+        if (instances.find(lea.getName()) == instances.end())
+        {
+            reply[lea.getName()] = llsd::map("desc", lea.getDesc());
+        }
     }
 }
 
+// Because LazyEventAPI deliberately mimics LLEventAPI's query API, this
+// function can be passed either -- even though they're unrelated types.
+template <typename API>
+void reportAPI(LLEventAPI::Response& reply, const API& api)
+{
+    reply["name"] = api.getName();
+    reply["desc"] = api.getDesc();
+    reply["key"]  = api.getDispatchKey();
+    LLSD ops;
+    for (const auto& namedesc : api)
+    {
+        ops.append(api.getMetadata(namedesc.first));
+    }
+    reply["ops"] = ops;
+}
+
 void LLLeapListener::getAPI(const LLSD& request) const
 {
     Response reply(LLSD(), request);
 
-    auto found = LLEventAPI::getInstance(request["api"]);
-    if (found)
+    // check first among existing LLEventAPI instances
+    auto foundea = LLEventAPI::getInstance(request["api"]);
+    if (foundea)
+    {
+        reportAPI(reply, *foundea);
+    }
+    else
     {
-        reply["name"] = found->getName();
-        reply["desc"] = found->getDesc();
-        reply["key"] = found->getDispatchKey();
-        LLSD ops;
-        for (LLEventAPI::const_iterator oi(found->begin()), oend(found->end());
-             oi != oend; ++oi)
+        // Here the requested LLEventAPI doesn't yet exist, but do we have a
+        // registered LazyEventAPI for it?
+        LL::LazyEventAPIBase::instance_snapshot snap;
+        auto foundlea = std::find_if(snap.begin(), snap.end(),
+                                     [api = request["api"].asString()]
+                                     (const auto& lea)
+                                     { return (lea.getName() == api); });
+        if (foundlea != snap.end())
         {
-            ops.append(found->getMetadata(oi->first));
+            reportAPI(reply, *foundlea);
         }
-        reply["ops"] = ops;
     }
 }
 
diff --git a/indra/llcommon/tests/lazyeventapi_test.cpp b/indra/llcommon/tests/lazyeventapi_test.cpp
index 4c78fd7105057ea0158c9ca6554e21e2668ecca5..31b2d6d17fdd8fb1bb8ab936f2b6c979fe642cf6 100644
--- a/indra/llcommon/tests/lazyeventapi_test.cpp
+++ b/indra/llcommon/tests/lazyeventapi_test.cpp
@@ -109,16 +109,28 @@ namespace tut
     {
         set_test_name("LazyEventAPI metadata");
         MyRegistrar regster;
+        // Of course we have 'regster' in hand; we don't need to search for
+        // it. But this next test verifies that we can find (all) LazyEventAPI
+        // instances using LazyEventAPIBase::instance_snapshot. Normally we
+        // wouldn't search; normally we'd just look at each instance in the
+        // loop body.
         const MyRegistrar* found = nullptr;
         for (const auto& registrar : LL::LazyEventAPIBase::instance_snapshot())
             if ((found = dynamic_cast<const MyRegistrar*>(&registrar)))
                 break;
         ensure("Failed to find MyRegistrar via LLInstanceTracker", found);
-        ensure_equals("wrong API name", found->mParams.name, "Test");
-        ensure_contains("wrong API desc", found->mParams.desc, "test LLEventAPI");
-        ensure_equals("wrong API field", found->mParams.field, "op");
-        ensure_equals("failed to find operations", found->mOperations.size(), 1);
-        ensure_equals("wrong operation name", found->mOperations[0].first, "set");
-        ensure_contains("wrong operation desc", found->mOperations[0].second, "set operation");
+
+        ensure_equals("wrong API name", found->getName(), "Test");
+        ensure_contains("wrong API desc", found->getDesc(), "test LLEventAPI");
+        ensure_equals("wrong API field", found->getDispatchKey(), "op");
+        // Normally we'd just iterate over *found. But for test purposes,
+        // actually capture the range of NameDesc pairs in a vector.
+        std::vector<LL::LazyEventAPIBase::NameDesc> ops{ found->begin(), found->end() };
+        ensure_equals("failed to find operations", ops.size(), 1);
+        ensure_equals("wrong operation name", ops[0].first, "set");
+        ensure_contains("wrong operation desc", ops[0].second, "set operation");
+        LLSD metadata{ found->getMetadata(ops[0].first) };
+        ensure_equals("bad metadata name", metadata["name"].asString(), ops[0].first);
+        ensure_equals("bad metadata desc", metadata["desc"].asString(), ops[0].second);
     }
 } // namespace tut