diff --git a/webv4/lib/avatars.js b/webv4/lib/avatars.js
index 24c8da2078f0c52f56345b30001882d3cdd9e1a6..a4b648799fe9b277f4536d963e90468155a8363a 100644
--- a/webv4/lib/avatars.js
+++ b/webv4/lib/avatars.js
@@ -14,9 +14,8 @@ function Avatars() {
             } else {
                 cache.local[usernumber] = data.data;
             }
-        } else {
-            return cache.local[usernumber];
         }
+        return cache.local[usernumber];
     }
 
     this.get_netuser = function (username, netaddr) {
@@ -30,9 +29,8 @@ function Avatars() {
             } else {
                 cache.network[netaddr][username] = data.data;
             }
-        } else {
-            return cache.network[netaddr][username];
         }
+        return cache.network[netaddr][username];
     }
 
 }
diff --git a/webv4/root/api/system.ssjs b/webv4/root/api/system.ssjs
index 26cca159d2281cdc73981c8f667f1933927b8f71..f19e1de7344310ae4113412c0d4bd75f17db16bc 100644
--- a/webv4/root/api/system.ssjs
+++ b/webv4/root/api/system.ssjs
@@ -5,87 +5,109 @@ var settings = load('modopts.js', 'web') || { web_directory: '../webv4' };
 load(settings.web_directory + '/lib/init.js');
 load(settings.web_lib + 'auth.js');
 
+var handled = false;
 var reply = {};
 
-if ((http_request.method === 'GET' || http_request.method === 'POST') &&
-	typeof http_request.query.call !== 'undefined' &&
-	user.number > 0
-) {
+if ((http_request.method === 'GET' || http_request.method === 'POST') && http_request.query.call !== undefined) {
 
 	switch (http_request.query.call[0]) {
-
-		case 'node-list':
-			var usr = new User(1);
-			reply = system.node_list.reduce(function (a, c, i) {
-				if (c.status !== 3) return a;
-				usr.number = c.useron;
-				a.push({
-					node: i,
-					status: format(NodeStatus[c.status], c.aux, c.extaux),
-					action: format(NodeAction[c.action], c.aux, c.extaux),
-					user: usr.alias,
-					connection: usr.connection
-				});
-				return a;
-			}, []);
-			for (var un = 1; un < system.lastuser; un++) {
-				usr.number = un;
-				if (usr.connection !== 'HTTP') continue;
-				if (usr.alias === settings.guest) continue;
-				if (usr.settings&USER_QUIET) continue;
-				if (usr.logontime < time() - settings.inactivity) continue;
-				var webAction = getSessionValue(usr.number, 'action');
-				if (webAction === null) continue;
-				reply.push({
-					status: '',
-					action: locale.strings.api_system.nodelist_action_prefix + ' ' + webAction,
-					user: usr.alias,
-					connection: 'W'
-				});
-			}
-			usr = undefined;
+		case 'get-avatar':
+			var avatar_lib = load({}, 'avatar_lib.js');
+			reply = http_request.query.user.map(function (e) {
+				const u = e.split('@');
+				var ret;
+				if (u.length === 1) {
+					ret = avatar_lib.read_localuser(u[0]) || {};
+				} else {
+					ret = avatar_lib.read_netuser(u[0], u[1]) || {};
+				}
+				ret.user = e;
+				return ret;
+			});
+			handled = true;
+			break;
+		default:
 			break;
+	}
 
-		case 'send-telegram':
-			if (user.alias === settings.guest) break;
-			if (typeof http_request.query.user === 'undefined') break;
-			if (typeof http_request.query.telegram === 'undefined' ||
-				http_request.query.telegram[0] === ''
-			) {
+	if (!handled && user.number > 0) {
+
+		switch (http_request.query.call[0]) {
+
+			case 'node-list':
+				var usr = new User(1);
+				reply = system.node_list.reduce(function (a, c, i) {
+					if (c.status !== 3) return a;
+					usr.number = c.useron;
+					a.push({
+						node: i,
+						status: format(NodeStatus[c.status], c.aux, c.extaux),
+						action: format(NodeAction[c.action], c.aux, c.extaux),
+						user: usr.alias,
+						connection: usr.connection
+					});
+					return a;
+				}, []);
+				for (var un = 1; un < system.lastuser; un++) {
+					usr.number = un;
+					if (usr.connection !== 'HTTP') continue;
+					if (usr.alias === settings.guest) continue;
+					if (usr.settings&USER_QUIET) continue;
+					if (usr.logontime < time() - settings.inactivity) continue;
+					var webAction = getSessionValue(usr.number, 'action');
+					if (webAction === null) continue;
+					reply.push({
+						status: '',
+						action: locale.strings.api_system.nodelist_action_prefix + ' ' + webAction,
+						user: usr.alias,
+						connection: 'W'
+					});
+				}
+				usr = undefined;
 				break;
-			}
-			if (http_request.query.telegram[0].length >
-				settings.maximum_telegram_length
-			) {
+
+			case 'send-telegram':
+				if (user.alias === settings.guest) break;
+				if (typeof http_request.query.user === 'undefined') break;
+				if (typeof http_request.query.telegram === 'undefined' ||
+					http_request.query.telegram[0] === ''
+				) {
+					break;
+				}
+				if (http_request.query.telegram[0].length >
+					settings.maximum_telegram_length
+				) {
+					break;
+				}
+				var un = system.matchuser(http_request.query.user[0]);
+				if (un < 1) break;
+				system.put_telegram(
+					un, format(
+						locale.strings.api_system.telegram_header_format,
+						user.alias, (new Date()).toLocaleString()
+					) + '\r\n' + utf8_decode(http_request.query.telegram[0]) + '\r\n'
+				);
 				break;
-			}
-			var un = system.matchuser(http_request.query.user[0]);
-			if (un < 1) break;
-			system.put_telegram(
-				un, format(
-					locale.strings.api_system.telegram_header_format,
-					user.alias, (new Date()).toLocaleString()
-				) + '\r\n' + utf8_decode(http_request.query.telegram[0]) + '\r\n'
-			);
-			break;
 
-		case 'get-telegram':
-			if (user.alias === settings.guest) break;
-			reply.telegram = system.get_telegram(user.number);
-			break;
+			case 'get-telegram':
+				if (user.alias === settings.guest) break;
+				reply.telegram = system.get_telegram(user.number);
+				break;
 
-		case 'set-xtrn-intent':
-			if (user.alias === settings.guest) break;
-			if (typeof http_request.query.code === 'undefined') break;
-			if (http_request.query.code[0].length > 8) break;
-			if (typeof xtrn_area.prog[http_request.query.code[0]] === 'undefined') {
+			case 'set-xtrn-intent':
+				if (user.alias === settings.guest) break;
+				if (typeof http_request.query.code === 'undefined') break;
+				if (http_request.query.code[0].length > 8) break;
+				if (typeof xtrn_area.prog[http_request.query.code[0]] === 'undefined') {
+					break;
+				}
+				setSessionValue(user.number, 'xtrn', http_request.query.code[0]);
 				break;
-			}
-			setSessionValue(user.number, 'xtrn', http_request.query.code[0]);
-			break;
 
-		default:
-			break;
+			default:
+				break;
+
+		}
 
 	}
 
diff --git a/webv4/root/js/avatars.js b/webv4/root/js/avatars.js
index bd04ecb0ce0f14b0d2e93b99650a00ffdbbc0c64..61b63e7e78c97eb084e384286a3c770cd580abad 100644
--- a/webv4/root/js/avatars.js
+++ b/webv4/root/js/avatars.js
@@ -37,3 +37,49 @@ function Avatarizer() {
     }
 
 }
+
+const Avatars = new (function () {
+
+    const gc = new GraphicsConverter('./images/cp437-ibm-vga8.png', 8, 16, 64, 4);
+
+    function draw(data) {
+        const img = new Image();
+        img.addEventListener('load', () => document.querySelectorAll(`div[data-avatar="${data.user}"]:empty`).forEach(e => e.appendChild(img.cloneNode(true))));
+        img.src = data.dataURL || 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFAAAABgCAYAAACKa/UAAAABiUlEQVR4Xu3c0YqCUBiFUXtKX1Gfcga6TPBgn6LDrG5zSy3377EQX8uy/ExeXwu8AH5t9w4CbH4Aox9AgFUg5p0Dnw44z3P8iC2+rmvbwSB9eQMBxuMHEOCugBGOBQEIcF/AKhwbAvC/Ad592TLyPruRpy8iAEeHcPA+QICHBIzwIa7txgABHhOwCh/z2mwNEGAUiHENBBgFYlwDAUaBGNdAgFEgxjUQYBSIcQ0EGAViXAMBRoEY10CAUSDGNRBgFIhxDQQYBWJcAwFGgRjXQIBRIMYf38DP73f33Vpng31+v9PvjQEYRwQgwEMCRvgQ13ZjgAD3BR6/Ct99mRILNFXgPMIA44N3AAJMZwEjHJ+pABBgmkCrcOObAD4e8K9fplwNPFxEAO4/+QjgoKKjn3oAAdazXPs3RwM1UAOvFYh7t4gAjAIxroEAo0CMayDAKBDjGggwCsS4BgKMAjGugQCjQIxrIMAoEOMaCDAKxLgGAowCMa6BAKNAjGsgwCgQ4xoIMArE+KiBv6PnKB+V8OyaAAAAAElFTkSuQmCC';
+    }
+
+    function cacheAvatar(data) {
+        if (data.data) {
+            gc.from_bin(atob(data.data), 10, 6, dataURL => {
+                data.dataURL = dataURL;
+                localStorage.setItem(`avatar-${data.user}`, JSON.stringify(data));
+                draw(data);
+            }, true);
+        } else {
+            data.data = null;
+            localStorage.setItem(`avatar-${data.user}`, JSON.stringify(data));
+            draw(data);
+        }
+    }
+
+    function fromCache(user) {
+        return JSON.parse(localStorage.getItem(`avatar-${user}`));
+        // should return null if local copy is aged; as it is, avatars will remain in local storage until user clears it
+    }
+
+    async function fetchAvatar(user) {
+        const u = [].concat(user);
+        const data = await v4_get(`./api/system.ssjs?call=get-avatar&user=${u.join('&user=')}`);
+        [].concat(data).forEach(cacheAvatar);
+    }
+
+    this.draw = async function (user) {
+        const u = [].concat(user).filter(e => {
+            const a = fromCache(e);
+            if (a === null) return true; // Not cached
+            if (a) draw(a);
+        });
+        if (u.length) fetchAvatar(u);
+    }
+
+})();
\ No newline at end of file