diff --git a/include/sway/input/cursor_spy.h b/include/sway/input/cursor_spy.h
new file mode 100644
index 00000000..478005c4
--- /dev/null
+++ b/include/sway/input/cursor_spy.h
@@ -0,0 +1,32 @@
+#ifndef _SWAY_INPUT_CURSOR_MONITOR_H
+#define _SWAY_INPUT_CURSOR_MONITOR_H
+
+#include "wayland-server-core.h"
+#include <wlr/types/wlr_seat.h>
+
+struct wp_pointer_spy_manager_v1 {
+  struct wl_global *global;
+  struct wl_list spies;
+
+  struct wl_listener display_destroy_listener;
+
+  void *data;
+};
+
+struct wp_pointer_spy_v1 {
+  struct wl_resource *resource;
+  struct wlr_seat *seat;
+  struct wl_list link; // wp_pointer_spy_manager_v1.spies
+
+  struct wl_listener seat_destroy;
+  struct wl_listener pointer_destroy;
+};
+
+struct wp_pointer_spy_manager_v1 *
+wp_pointer_spy_manager_v1_create(struct wl_display *display);
+
+void wp_pointer_spy_manager_v1_send_motion(
+    struct wp_pointer_spy_manager_v1 *manager, struct wlr_seat *seat,
+    uint32_t time, double lx, double ly);
+
+#endif
diff --git a/include/sway/server.h b/include/sway/server.h
index ef70ae4e..89b4c3db 100644
--- a/include/sway/server.h
+++ b/include/sway/server.h
@@ -21,6 +21,7 @@
 #include "config.h"
 #include "list.h"
 #include "sway/desktop/idle_inhibit_v1.h"
+#include "sway/input/cursor_spy.h"
 #if HAVE_XWAYLAND
 #include "sway/xwayland.h"
 #endif
@@ -69,6 +70,7 @@ struct sway_server {
 #endif
 
 	struct wlr_relative_pointer_manager_v1 *relative_pointer_manager;
+	struct wp_pointer_spy_manager_v1 *pointer_spy_manager;
 
 	struct wlr_server_decoration_manager *server_decoration_manager;
 	struct wl_listener server_decoration;
diff --git a/protocols/meson.build b/protocols/meson.build
index 2992ac58..d000847b 100644
--- a/protocols/meson.build
+++ b/protocols/meson.build
@@ -18,6 +18,7 @@ protocols = [
 	'idle.xml',
 	'wlr-input-inhibitor-unstable-v1.xml',
 	'wlr-output-power-management-unstable-v1.xml',
+	'wp-pointer-spy-v1.xml'
 ]
 
 wl_protos_src = []
diff --git a/protocols/wp-pointer-spy-v1.xml b/protocols/wp-pointer-spy-v1.xml
new file mode 100644
index 00000000..3cda867e
--- /dev/null
+++ b/protocols/wp-pointer-spy-v1.xml
@@ -0,0 +1,65 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<protocol name="wp_pointer_spy_v1">
+  <copyright>
+    Copyright © 2017 ext0l
+
+    Permission to use, copy, modify, distribute, and sell this
+    software and its documentation for any purpose is hereby granted
+    without fee, provided that the above copyright notice appear in
+    all copies and that both that copyright notice and this permission
+    notice appear in supporting documentation, and that the name of
+    the copyright holders not be used in advertising or publicity
+    pertaining to distribution of the software without specific,
+    written prior permission.  The copyright holders make no
+    representations about the suitability of this software for any
+    purpose.  It is provided "as is" without express or implied
+    warranty.
+
+    THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS
+    SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
+    FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY
+    SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+    WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
+    AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION,
+    ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
+    THIS SOFTWARE.
+  </copyright>
+
+  <interface name="wp_pointer_spy_manager_v1" version="1">
+    <description summary="monitor a pointer's position across an output">
+      This interface allows clients to know the position of the pointer at all
+      times. This is intended for 'desktop toy'-type applications. This is
+      purely an experiment and isn't intended for serious usage.
+    </description>
+
+    <request name="get_pointer_spy">
+      <description summary="start monitoring a pointer">
+        Create a new pointer spy object.
+      </description>
+      <arg name="id" type="new_id" interface="wp_pointer_spy_v1"/>
+      <arg name="pointer" type="object" interface="wl_pointer"/>
+    </request>
+  </interface>
+
+  <interface name="wp_pointer_spy_v1" version="1">
+    <description summary="monitors a pointer's position">
+      While this exists, it will receive events every time the pointer moves.
+    </description>
+
+    <event name="motion">
+      <description summary="pointer motion">
+        Sent whenever the pointer moves.
+      </description>
+
+      <arg name="time" type="uint" summary="timestamp with millisecond granularity" />
+      <arg name="x" type="fixed" summary="output-local x coordinate" />
+      <arg name="y" type="fixed" summary="output-local y coordinate" />
+    </event>
+
+    <request name="destroy" type="destructor">
+      <description summary="destroy the pointer_spy object">
+        Destroys the pointer spy object.
+      </description>
+    </request>
+  </interface>
+</protocol>
diff --git a/sway/input/cursor.c b/sway/input/cursor.c
index 36aab93e..fe50b883 100644
--- a/sway/input/cursor.c
+++ b/sway/input/cursor.c
@@ -20,6 +20,7 @@
 #include "util.h"
 #include "sway/commands.h"
 #include "sway/desktop.h"
+#include "sway/input/cursor_spy.h"
 #include "sway/input/cursor.h"
 #include "sway/input/keyboard.h"
 #include "sway/input/tablet.h"
@@ -373,6 +374,8 @@ void pointer_motion(struct sway_cursor *cursor, uint32_t time_msec,
 		cursor->seat->wlr_seat, (uint64_t)time_msec * 1000,
 		dx, dy, dx_unaccel, dy_unaccel);
 
+	wp_pointer_spy_manager_v1_send_motion(server.pointer_spy_manager, cursor->seat->wlr_seat, time_msec, cursor->cursor->x, cursor->cursor->y);
+
 	// Only apply pointer constraints to real pointer input.
 	if (cursor->active_constraint && device->type == WLR_INPUT_DEVICE_POINTER) {
 		struct wlr_surface *surface = NULL;
diff --git a/sway/input/cursor_spy.c b/sway/input/cursor_spy.c
new file mode 100644
index 00000000..4a31bd27
--- /dev/null
+++ b/sway/input/cursor_spy.c
@@ -0,0 +1,151 @@
+#include "sway/input/cursor_spy.h"
+#include "wayland-server-core.h"
+#include "wayland-server-protocol.h"
+#include "wayland-util.h"
+#include "wp-pointer-spy-v1-protocol.h"
+#include <assert.h>
+#include <stdlib.h>
+#include <wlr/types/wlr_seat.h>
+
+static const struct wp_pointer_spy_manager_v1_interface manager_impl;
+static const struct wp_pointer_spy_v1_interface spy_impl;
+
+// throat-clearing utilities
+
+static struct wp_pointer_spy_manager_v1 *
+manager_from_resource(struct wl_resource *resource) {
+  assert(wl_resource_instance_of(resource, &wp_pointer_spy_manager_v1_interface,
+                                 &manager_impl));
+  return wl_resource_get_user_data(resource);
+}
+
+static struct wp_pointer_spy_v1 *
+spy_from_resource(struct wl_resource *resource) {
+  assert(wl_resource_instance_of(resource, &wp_pointer_spy_v1_interface,
+                                 &spy_impl));
+  return wl_resource_get_user_data(resource);
+}
+
+// actual implementation of spy_destroy
+static void spy_destroy(struct wp_pointer_spy_v1 *spy) {
+  wl_list_remove(&spy->link);
+  wl_list_remove(&spy->seat_destroy.link);
+  wl_list_remove(&spy->pointer_destroy.link);
+  wl_resource_set_user_data(spy->resource, NULL);
+  free(spy);
+}
+
+// destroy given the wl_resource *
+static void spy_destroy_from_resource(struct wl_resource *resource) {
+  struct wp_pointer_spy_v1 *spy = spy_from_resource(resource);
+  if (spy) {
+    spy_destroy(spy);
+  }
+}
+
+// used to implement the destroy() request
+static void spy_impl_handle_destroy(struct wl_client *client,
+                                    struct wl_resource *resource) {
+  spy_destroy_from_resource(resource);
+}
+
+// used for the seat_destroy listener
+static void spy_handle_seat_destroy(struct wl_listener *listener, void *data) {
+  struct wp_pointer_spy_v1 *spy = wl_container_of(listener, spy, seat_destroy);
+  spy_destroy(spy);
+}
+
+// used for the pointer_destroy listener
+static void spy_handle_pointer_destroy(struct wl_listener *listener,
+                                       void *data) {
+  struct wp_pointer_spy_v1 *spy =
+      wl_container_of(listener, spy, pointer_destroy);
+  spy_destroy(spy);
+}
+
+// Constructs the new pointer spy and hooks up necessary libwayland infrastructure
+static void manager_handle_get_pointer_spy(
+    struct wl_client *client, struct wl_resource *manager_resource, uint32_t id,
+    struct wl_resource *pointer_resource) {
+  struct wp_pointer_spy_manager_v1 *manager =
+      manager_from_resource(manager_resource);
+
+  struct wl_resource *resource =
+      wl_resource_create(client, &wp_pointer_spy_v1_interface,
+                         wl_resource_get_version(manager_resource), id);
+  if (resource == NULL) {
+    wl_client_post_no_memory(client);
+    return;
+  }
+
+  struct wp_pointer_spy_v1 *spy = calloc(1, sizeof(*spy));
+  if (spy == NULL) {
+    wl_client_post_no_memory(client);
+    return;
+  }
+
+  wl_resource_set_implementation(resource, &spy_impl, spy, spy_destroy_from_resource);
+
+  spy->seat = wlr_seat_client_from_pointer_resource(pointer_resource)->seat;
+  spy->resource = resource;
+  wl_list_insert(&manager->spies, &spy->link);
+
+  wl_resource_add_destroy_listener(pointer_resource,
+                                   &spy->pointer_destroy);
+  spy->pointer_destroy.notify = spy_handle_pointer_destroy;
+  wl_signal_add(&spy->seat->events.destroy, &spy->seat_destroy);
+  spy->seat_destroy.notify = spy_handle_seat_destroy;
+}
+
+static const struct wp_pointer_spy_manager_v1_interface manager_impl = {
+    .get_pointer_spy = manager_handle_get_pointer_spy,
+};
+
+static const struct wp_pointer_spy_v1_interface spy_impl = {
+    .destroy = spy_impl_handle_destroy,
+};
+
+static void bind(struct wl_client *client, void *data, uint32_t version,
+                 uint32_t id) {
+  struct wp_pointer_spy_manager_v1 *manager = data;
+  struct wl_resource *resource = wl_resource_create(
+      client, &wp_pointer_spy_manager_v1_interface, version, id);
+  if (resource == NULL) {
+    wl_client_post_no_memory(client);
+    return;
+  }
+
+  wl_resource_set_implementation(resource, &manager_impl, manager, NULL);
+}
+
+struct wp_pointer_spy_manager_v1 *
+wp_pointer_spy_manager_v1_create(struct wl_display *display) {
+  struct wp_pointer_spy_manager_v1 *manager = calloc(1, sizeof(*manager));
+  if (manager == NULL) {
+    return NULL;
+  }
+
+  wl_list_init(&manager->spies);
+
+  manager->global = wl_global_create(
+      display, &wp_pointer_spy_manager_v1_interface, 1, manager, bind);
+  if (manager->global == NULL) {
+    free(manager);
+    return NULL;
+  }
+
+  return manager;
+}
+
+void wp_pointer_spy_manager_v1_send_motion(
+    struct wp_pointer_spy_manager_v1 *manager, struct wlr_seat *seat,
+    uint32_t time, double x, double y) {
+  struct wp_pointer_spy_v1 *spy;
+  wl_list_for_each(spy, &manager->spies, link) {
+    if (spy->seat != seat) {
+      continue;
+    }
+    wp_pointer_spy_v1_send_motion(spy->resource, time, wl_fixed_from_double(x),
+                                  wl_fixed_from_double(y));
+  }
+}
diff --git a/sway/meson.build b/sway/meson.build
index 3abd778d..6694d09e 100644
--- a/sway/meson.build
+++ b/sway/meson.build
@@ -25,6 +25,7 @@ sway_sources = files(
 
 	'input/input-manager.c',
 	'input/cursor.c',
+	'input/cursor_spy.c',
 	'input/keyboard.c',
 	'input/seat.c',
 	'input/seatop_default.c',
diff --git a/sway/server.c b/sway/server.c
index 9edc8bfc..04554176 100644
--- a/sway/server.c
+++ b/sway/server.c
@@ -42,6 +42,7 @@
 #include "log.h"
 #include "sway/config.h"
 #include "sway/desktop/idle_inhibit_v1.h"
+#include "sway/input/cursor_spy.h"
 #include "sway/input/input-manager.h"
 #include "sway/output.h"
 #include "sway/server.h"
@@ -101,7 +102,8 @@ static bool is_privileged(const struct wl_global *global) {
 		global == server.input->inhibit->global ||
 		global == server.input->keyboard_shortcuts_inhibit->global ||
 		global == server.input->virtual_keyboard->global ||
-		global == server.input->virtual_pointer->global;
+		global == server.input->virtual_pointer->global ||
+		global == server.pointer_spy_manager->global;
 }
 
 static bool filter_global(const struct wl_client *client,
@@ -221,6 +223,7 @@ bool server_init(struct sway_server *server) {
 
 	server->relative_pointer_manager =
 		wlr_relative_pointer_manager_v1_create(server->wl_display);
+	server->pointer_spy_manager = wp_pointer_spy_manager_v1_create(server->wl_display);
 
 	server->pointer_constraints =
 		wlr_pointer_constraints_v1_create(server->wl_display);
