diff --git a/apps/routerconsole/java/src/net/i2p/router/sybil/Analysis.java b/apps/routerconsole/java/src/net/i2p/router/sybil/Analysis.java
index c6f878418efb442e5d8d6c666469e7270d288c18..632baae09d8069a8778f4b4ad10bad49fce0a89b 100644
--- a/apps/routerconsole/java/src/net/i2p/router/sybil/Analysis.java
+++ b/apps/routerconsole/java/src/net/i2p/router/sybil/Analysis.java
@@ -11,6 +11,9 @@ import java.util.List;
 import java.util.Map;
 import java.util.Set;
 
+import net.i2p.app.ClientAppManager;
+import net.i2p.app.ClientAppState;
+import static net.i2p.app.ClientAppState.*;
 import net.i2p.data.DataHelper;
 import net.i2p.data.Destination;
 import net.i2p.data.Hash;
@@ -20,6 +23,7 @@ import net.i2p.data.router.RouterInfo;
 import net.i2p.data.router.RouterKeyGenerator;
 import net.i2p.router.RouterContext;
 import net.i2p.router.TunnelPoolSettings;
+import net.i2p.router.app.RouterApp;
 import net.i2p.router.crypto.FamilyKeyCrypto;
 import net.i2p.router.peermanager.DBHistory;
 import net.i2p.router.peermanager.PeerProfile;
@@ -36,10 +40,20 @@ import net.i2p.util.ObjectCounter;
  *  @since 0.9.38 split out from SybilRenderer
  *
  */
-public class Analysis {
+public class Analysis implements RouterApp {
 
     private final RouterContext _context;
+    private final ClientAppManager _cmgr;
+    private final PersistSybil _persister;
+    private volatile ClientAppState _state = UNINITIALIZED;
     private final DecimalFormat fmt = new DecimalFormat("#0.00");
+    /**
+     *  The name we register with the ClientAppManager
+     */
+    public static final String APP_NAME = "sybil";
+    public static final String PROP_FREQUENCY = "router.sybilFrequency";
+    private static final long MIN_FREQUENCY = 60*60*1000L;
+    private static final long MIN_UPTIME = 75*60*1000L;
 
     public static final int PAIRMAX = 20;
     public static final int MAX = 10;
@@ -64,8 +78,88 @@ public class Analysis {
     private static final double POINTS_NEW = 4.0;
     private static final double POINTS_BANLIST = 25.0;
 
-    public Analysis(RouterContext ctx) {
+    /** Get via getInstance() */
+    private Analysis(RouterContext ctx, ClientAppManager mgr, String[] args) {
         _context = ctx;
+        _cmgr = mgr;
+        _persister = new PersistSybil(ctx);
+    }
+
+    /**
+     *  @return non-null, creates new if not already registered
+     */
+    public synchronized static Analysis getInstance(RouterContext ctx) {
+        ClientAppManager cmgr = ctx.clientAppManager();
+        if (cmgr == null)
+            return null;
+        Analysis rv = (Analysis) cmgr.getRegisteredApp(APP_NAME);
+        if (rv == null) {
+            rv = new Analysis(ctx, cmgr, null);
+            rv.startup();
+        }
+        return rv;
+    }
+
+    public PersistSybil getPersister() { return _persister; }
+
+    /**
+     *  ClientApp interface
+     */
+    public synchronized void startup() {
+        changeState(STARTING);
+        changeState(RUNNING);
+        _cmgr.register(this);
+        schedule();
+    }
+
+    /**
+     *  ClientApp interface
+     *  @param args ignored
+     */
+    public synchronized void shutdown(String[] args) {
+        if (_state == STOPPED)
+            return;
+        changeState(STOPPING);
+        changeState(STOPPED);
+    }
+
+    public ClientAppState getState() {
+        return _state;
+    }
+
+    public String getName() {
+        return APP_NAME;
+    }
+
+    public String getDisplayName() {
+        return "Sybil Analyzer";
+    }
+
+    /////// end ClientApp methods
+
+    private synchronized void changeState(ClientAppState state) {
+        _state = state;
+        if (_cmgr != null)
+            _cmgr.notify(this, state, null, null);
+    }
+
+    public void schedule() {
+        long freq = _context.getProperty(PROP_FREQUENCY, 0L);
+        if (freq > 0) {
+            List<Long> previous = _persister.load();
+            long now = _context.clock().now();
+            long when;
+            if (!previous.isEmpty()) {
+                if (freq < MIN_FREQUENCY)
+                    freq = MIN_FREQUENCY;
+                when = Math.max(previous.get(0).longValue() + freq, now);
+            } else {
+                when = now;
+            }
+            long up = _context.router().getUptime();
+            when = Math.max(when, now + MIN_UPTIME - up);
+            // TODO schedule for when
+        }
     }
 
     private static class RouterInfoRoutingKeyComparator implements Comparator<RouterInfo>, Serializable {
diff --git a/apps/routerconsole/java/src/net/i2p/router/sybil/PersistSybil.java b/apps/routerconsole/java/src/net/i2p/router/sybil/PersistSybil.java
index 7efa5a5cde8e8bbe383e71617ba63b4b76e2ca2b..23ad7f3b055c31b44ab44bdbf71d06f1da718cb0 100644
--- a/apps/routerconsole/java/src/net/i2p/router/sybil/PersistSybil.java
+++ b/apps/routerconsole/java/src/net/i2p/router/sybil/PersistSybil.java
@@ -44,7 +44,8 @@ public class PersistSybil {
     private static final String PFX = "sybil-";
     private static final String SFX = ".txt.gz";
 
-    public PersistSybil(I2PAppContext ctx) {
+    /** access via Analysis.getPersister() */
+    PersistSybil(I2PAppContext ctx) {
         _context = ctx;
         _log = ctx.logManager().getLog(PersistSybil.class);
     }
diff --git a/apps/routerconsole/java/src/net/i2p/router/web/RouterConsoleRunner.java b/apps/routerconsole/java/src/net/i2p/router/web/RouterConsoleRunner.java
index 7410e7140d78a0a5eb5b0f914f29ed7794f03fde..59881aa72b1b9d1d190194d317941ca3dca166ec 100644
--- a/apps/routerconsole/java/src/net/i2p/router/web/RouterConsoleRunner.java
+++ b/apps/routerconsole/java/src/net/i2p/router/web/RouterConsoleRunner.java
@@ -34,6 +34,7 @@ import net.i2p.jetty.I2PLogger;
 import net.i2p.router.RouterContext;
 import net.i2p.router.app.RouterApp;
 import net.i2p.router.news.NewsManager;
+import net.i2p.router.sybil.Analysis;
 import net.i2p.router.update.ConsoleUpdateManager;
 import net.i2p.util.Addresses;
 import net.i2p.util.FileSuffixFilter;
@@ -865,6 +866,13 @@ public class RouterConsoleRunner implements RouterApp {
             if (_mgr == null)
                 _context.addShutdownTask(new ServerShutdown());
             ConfigServiceHandler.registerSignalHandler(_context);
+
+            if (_mgr != null &&
+                _context.getBooleanProperty(HelperBase.PROP_ADVANCED) &&
+                _context.getProperty(Analysis.PROP_FREQUENCY, 0L) > 0) {
+                // registers and starts itself
+                Analysis.getInstance(_context);
+            }
     }
     
     /**
diff --git a/apps/routerconsole/java/src/net/i2p/router/web/helpers/SybilRenderer.java b/apps/routerconsole/java/src/net/i2p/router/web/helpers/SybilRenderer.java
index 4b18a85418b7fdc4ba7f93cf895f21713bb4f2af..d7a96a5d0f66c32771ed6ef734b2294e65968f3e 100644
--- a/apps/routerconsole/java/src/net/i2p/router/web/helpers/SybilRenderer.java
+++ b/apps/routerconsole/java/src/net/i2p/router/web/helpers/SybilRenderer.java
@@ -125,7 +125,7 @@ public class SybilRenderer {
      */
     private void renderRouterInfoHTML(Writer out, int mode, long date) throws IOException {
         Hash us = _context.routerHash();
-        Analysis analysis = new Analysis(_context);
+        Analysis analysis = Analysis.getInstance(_context);
         List<RouterInfo> ris = analysis.getFloodfills(us);
         if (ris.isEmpty()) {
             out.write("<h3 class=\"sybils\">No known floodfills</h3>");
@@ -180,7 +180,7 @@ public class SybilRenderer {
         } else if (mode == 11) {
             renderDestSummary(out, buf, analysis, avgMinDist, ris, points);
         } else if (mode == 12) {
-            PersistSybil ps = new PersistSybil(_context);
+            PersistSybil ps = analysis.getPersister();
             try {
                 points = ps.load(date);
             } catch (IOException ioe) {
@@ -196,7 +196,7 @@ public class SybilRenderer {
             long now = _context.clock().now();
             points = analysis.backgroundAnalysis();
             if (!points.isEmpty()) {
-                PersistSybil ps = new PersistSybil(_context);
+                PersistSybil ps = analysis.getPersister();
                 try {
                     ps.store(now, points);
                 } catch (IOException ioe) {
@@ -214,7 +214,7 @@ public class SybilRenderer {
      *  @since 0.9.38
      */
     private void renderOverview(Writer out, StringBuilder buf, Analysis analysis) throws IOException {
-        PersistSybil ps = new PersistSybil(_context);
+        PersistSybil ps = analysis.getPersister();
         List<Long> dates = ps.load();
         if (dates.isEmpty()) {
             out.write("No stored analysis");