diff --git a/docs/administration.md b/docs/administration.md
index c952549..3a7111f 100644
--- a/docs/administration.md
+++ b/docs/administration.md
@@ -68,6 +68,7 @@ are:
   * [**modify_account_status**](#modify_account_status): `{"status": "active"|"hidden"|"inactive", "addresses":[...]}`
   * [**reject_requests**](#reject_requests): `{"type": "import"|"create", "addresses":[...]}`
   * [**rescan**](#rescan): `{"height":..., "addresses":[...]}`
+  * [**validate**](#validate): `{"spend_public_hex":..., "view_public_hex":..., "view_key_hex":...}`
   * [**webhook_add**](#webhook_add): `{"type":"tx-confirmation", "address":"...", "url":"...", ...}` with optional fields:
     * **token**: A string to be returned when the webhook is triggered
     * **payment_id**: 16 hex characters representing a unique identifier for a transaction
@@ -143,6 +144,46 @@ information from that endpoint on how to use this one.
 This tells the scanner to rescan specific account(s) from the specified
 height.
 
+### validate
+This takes the spend_public, view_public, and view key all as hex, and then
+does basic validation for the caller: (1) that each value is 64 hex-ascii
+characters, (2) that the public keys are valid ed25519 points, and that (3)
+the view_key matches the view_public.
+
+The return value is the `address` on success, and `error` object on
+validation failure:
+
+#### Example Request
+```json
+{
+  "params": {
+    "view_public_hex": "3e77c1ee5a396cd6c3f68ce343882b2a09317649d6501078911fb17a1adac0b6",
+    "spend_public_hex": "7028d918af2cc4f1cfa499cca2ef014e57124982b99004e9630caf0b25c13954",
+    "view_key_hex": "80..."
+  },
+  "auth": "f50922f5fcd186eaa4bd7070b8072b66fea4fd736f06bd82df702e2314187d09"
+}
+```
+
+#### Example Failure Return
+```json
+{
+  "error": {
+    "field": "view_public_hex",
+    "details": "Invalid public key format"
+  }
+}
+```
+HTTP error codes are still returned if the JSON itself is invalid.
+
+#### Example Success Return
+```json
+{
+  "address": "9wRAu3giCtKhSsVnkZJ7LLE6zqzrmMKpPg39S8aoC7T6F6GobeDpz8TcvVfTQT3ucW82oTYKG8v3ZMAeh8SZVXWwMdvwZew"
+}
+```
+
+
 ### webhook_add
 This is used to track events happening in the database: (1) a new payment to
 an optional payment_id, or (2) a new account creation. This endpoint always
diff --git a/src/rest_server.cpp b/src/rest_server.cpp
index bdbb651..814c955 100644
--- a/src/rest_server.cpp
+++ b/src/rest_server.cpp
@@ -777,6 +777,7 @@ namespace lws
       {"/modify_account_status", call_admin<rpc::modify_account_>,  50 * 1024},
       {"/reject_requests",       call_admin<rpc::reject_requests_>, 50 * 1024},
       {"/rescan",                call_admin<rpc::rescan_>,          50 * 1024},
+      {"/validate",              call_admin<rpc::validate_>,        50 * 1024},
       {"/webhook_add",           call_admin<rpc::webhook_add_>,     50 * 1024},
       {"/webhook_delete",        call_admin<rpc::webhook_delete_>,  50 * 1024},
       {"/webhook_delete_uuid",   call_admin<rpc::webhook_del_uuid_>,50 * 1024},
diff --git a/src/rpc/admin.cpp b/src/rpc/admin.cpp
index 87c0e7f..98a986c 100644
--- a/src/rpc/admin.cpp
+++ b/src/rpc/admin.cpp
@@ -167,6 +167,10 @@ namespace lws { namespace rpc
   {
     read_addresses(source, self, WIRE_FIELD(height));
   }
+  void read_bytes(wire::reader& source, validate_req& self)
+  {
+    wire::object(source, WIRE_FIELD(spend_public_hex), WIRE_FIELD(view_public_hex), WIRE_FIELD(view_key_hex));
+  }
   void read_bytes(wire::reader& source, webhook_add_req& self)
   {
     boost::optional<std::string> address;
@@ -237,6 +241,67 @@ namespace lws { namespace rpc
     return write_addresses(dest, disk.rescan(req.height, epee::to_span(req.addresses)));
   }
 
+  namespace
+  {
+    struct validate_error
+    {
+      std::string field;
+      std::string details;
+    };
+
+    void write_bytes(wire::writer& dest, const validate_error& self)
+    {
+      wire::object(dest, WIRE_FIELD(field), WIRE_FIELD(details));
+    }
+
+    expect<void> output_error(wire::writer& dest, std::string field, std::string details)
+    {
+      wire::object(dest, wire::field("error", validate_error{std::move(field), std::move(details)}));
+      return success();
+    }
+
+    template<typename T>
+    bool convert_key(wire::writer& dest, T& out, const boost::string_ref in, const boost::string_ref field)
+    {
+      if (in.size() != sizeof(out) * 2)
+      {
+        output_error(dest, std::string{field}, "Expected " + std::to_string(sizeof(out) * 2) + " characters");
+        return false;
+      }
+      if (!epee::from_hex::to_buffer(epee::as_mut_byte_span(out), in))
+      {
+        output_error(dest, std::string{field}, "Invalid hex");
+        return false;
+      }
+      return true;
+    }
+  }
+
+  expect<void> validate_::operator()(wire::writer& dest, const db::storage&, const request& req) const
+  {
+    db::account_address address{};
+    crypto::secret_key view_key{};
+
+    if (!convert_key(dest, address.spend_public, req.spend_public_hex, "spend_public_hex"))
+      return success(); // error is delivered in JSON as opposed to HTTP codes
+    if (!convert_key(dest, address.view_public, req.view_public_hex, "view_public_hex"))
+      return success();
+    if (!convert_key(dest, unwrap(unwrap(view_key)), req.view_key_hex, "view_key_hex"))
+      return success();
+
+    if (!crypto::check_key(address.spend_public))
+      return output_error(dest, "spend_public_hex", "Invalid public key format");
+    if (!crypto::check_key(address.view_public))
+      return output_error(dest, "view_public_hex", "Invalid public key format");
+
+    crypto::public_key test{};
+    if (!crypto::secret_key_to_public_key(view_key, test) || test != address.view_public)
+      return output_error(dest, "view_key_hex", "view_key and view_public do not match");
+
+    wire::object(dest, wire::field("address", db::address_string(address)));
+    return success();
+  }
+
   expect<void> webhook_add_::operator()(wire::writer& dest, db::storage disk, request&& req) const
   {
     switch (req.type)
diff --git a/src/rpc/admin.h b/src/rpc/admin.h
index 84b0956..7344387 100644
--- a/src/rpc/admin.h
+++ b/src/rpc/admin.h
@@ -70,6 +70,14 @@ namespace rpc
   };
   void read_bytes(wire::reader&, rescan_req&);
 
+  struct validate_req
+  {
+    std::string spend_public_hex;
+    std::string view_public_hex;
+    std::string view_key_hex;
+  };
+  void read_bytes(wire::reader&, validate_req&);
+
   struct webhook_add_req
   {
     std::string url;
@@ -147,6 +155,13 @@ namespace rpc
   };
   constexpr const rescan_ rescan{};
 
+  struct validate_
+  {
+    using request = validate_req;
+    expect<void> operator()(wire::writer& dest, const db::storage&, const request& req) const;
+  };
+  constexpr const validate_ validate{};
+
   struct webhook_add_
   {
     using request = webhook_add_req;
@@ -177,5 +192,4 @@ namespace rpc
     { return (*this)(dest, std::move(disk)); }
   };
   constexpr const webhook_list_ webhook_list{};
-
 }} // lws // rpc