diff --git a/src/assets/avatars/BotDom_Avatar.glb b/src/assets/avatars/BotDom_Avatar.glb
new file mode 100644
index 0000000000000000000000000000000000000000..9c72e3d2fc170d6b0782d741eca441a62780daad
Binary files /dev/null and b/src/assets/avatars/BotDom_Avatar.glb differ
diff --git a/src/assets/avatars/BotDom_Avatar_Unlit.glb b/src/assets/avatars/BotDom_Avatar_Unlit.glb
new file mode 100644
index 0000000000000000000000000000000000000000..30b6af80ec3ab756eca52ff426e184890260e463
Binary files /dev/null and b/src/assets/avatars/BotDom_Avatar_Unlit.glb differ
diff --git a/src/assets/avatars/BotGreg_Avatar.glb b/src/assets/avatars/BotGreg_Avatar.glb
new file mode 100644
index 0000000000000000000000000000000000000000..92dc4e48291683d740b47c59871a62a8f1730ea4
Binary files /dev/null and b/src/assets/avatars/BotGreg_Avatar.glb differ
diff --git a/src/assets/avatars/BotGreg_Avatar_Unlit.glb b/src/assets/avatars/BotGreg_Avatar_Unlit.glb
new file mode 100644
index 0000000000000000000000000000000000000000..5aa65d9c30c90bdbacbe18df0a3a32041ef195f2
Binary files /dev/null and b/src/assets/avatars/BotGreg_Avatar_Unlit.glb differ
diff --git a/src/assets/avatars/BotPinky_Avatar.glb b/src/assets/avatars/BotPinky_Avatar.glb
new file mode 100644
index 0000000000000000000000000000000000000000..b41c3b4cadc810fddad7f13e15dcdf353f60a4df
Binary files /dev/null and b/src/assets/avatars/BotPinky_Avatar.glb differ
diff --git a/src/assets/avatars/BotPinky_Avatar_Unlit.glb b/src/assets/avatars/BotPinky_Avatar_Unlit.glb
new file mode 100644
index 0000000000000000000000000000000000000000..4b4cc3d36a5e05c7f2f84a4ac45a10153299e404
Binary files /dev/null and b/src/assets/avatars/BotPinky_Avatar_Unlit.glb differ
diff --git a/src/assets/avatars/BotRobert_Avatar.glb b/src/assets/avatars/BotRobert_Avatar.glb
new file mode 100644
index 0000000000000000000000000000000000000000..8cb2a57c63234639c0913717f2e13610e4ecbb21
Binary files /dev/null and b/src/assets/avatars/BotRobert_Avatar.glb differ
diff --git a/src/assets/avatars/BotRobert_Avatar_Unlit.glb b/src/assets/avatars/BotRobert_Avatar_Unlit.glb
new file mode 100644
index 0000000000000000000000000000000000000000..e457f49cf3a5f5ad3e48399ec4a91b405e5bb598
Binary files /dev/null and b/src/assets/avatars/BotRobert_Avatar_Unlit.glb differ
diff --git a/src/assets/avatars/avatars.json b/src/assets/avatars/avatars.json
new file mode 100644
index 0000000000000000000000000000000000000000..04642a1278ad0ee48f6aba5d5621d19eb2f5e906
--- /dev/null
+++ b/src/assets/avatars/avatars.json
@@ -0,0 +1,39 @@
+{
+  "avatars": [
+    {
+      "id": "botdefault",
+      "models": {
+        "low": "BotDefault_Avatar_Unlit.glb",
+        "high": "BotDefault_Avatar.glb"
+      }
+    },
+    {
+      "id": "botdom",
+      "models": {
+        "low": "BotDom_Avatar_Unlit.glb",
+        "high": "BotDom_Avatar.glb"
+      }
+    },
+    {
+      "id": "botgreg",
+      "models": {
+        "low": "BotGreg_Avatar_Unlit.glb",
+        "high": "BotGreg_Avatar.glb"
+      }
+    },
+    {
+      "id": "botpinky",
+      "models": {
+        "low": "BotPinky_Avatar_Unlit.glb",
+        "high": "BotPinky_Avatar.glb"
+      }
+    },
+    {
+      "id": "botrobert",
+      "models": {
+        "low": "BotRobert_Avatar_Unlit.glb",
+        "high": "BotRobert_Avatar.glb"
+      }
+    }
+  ]
+}
diff --git a/src/assets/stylesheets/avatar-selector.scss b/src/assets/stylesheets/avatar-selector.scss
new file mode 100644
index 0000000000000000000000000000000000000000..0f75add66f7f42953eccaa3d6279840b3e3f3e0c
--- /dev/null
+++ b/src/assets/stylesheets/avatar-selector.scss
@@ -0,0 +1,27 @@
+#selector-root {
+  height: 100%;
+}
+.avatar-selector {
+  overflow: hidden; 
+  height: 100%;
+  &__prev-button, &__next-button {
+    position: absolute; 
+    top: 50%;
+    margin-top: -4em;
+    appearance: none;
+    -moz-appearance: none;
+    -webkit-appearance: none;
+    background: transparent;
+    color: white;
+    border: none;
+  }
+  &__prev-button {
+    left: -2em;
+  }
+  &__next-button {
+    right: -2em;
+  }
+  &__button-icon {
+    font-size: 84pt;
+  }
+}
diff --git a/src/assets/stylesheets/profile.scss b/src/assets/stylesheets/profile.scss
index 05b2e5d801f42cee41a9e245eb57daed200798cc..408b55368b7cf2246a1fd5534e4b9b1e896fa826 100644
--- a/src/assets/stylesheets/profile.scss
+++ b/src/assets/stylesheets/profile.scss
@@ -8,16 +8,22 @@
   align-items: center;
   display: flex;
   pointer-events: auto;
+   
+  &__avatar-selector {
+    border: none;
+    width: 280px;
+    height: 300px;
+    margin: 1em 0;
+  }
 }
 
 .profile-entry__box {
-  height: 150px;
   border-radius: 8px;
   display: flex;
   flex-direction: column;
   justify-content: space-between;
   align-items: center;
-  padding: 25px;
+  padding: 15px;
   flex: 1 1 100%;
 }
 
@@ -42,6 +48,7 @@
   line-height: 2.0em;
   padding-left: 1.25em;
   padding-right: 1.25em;
+  margin: 0.5em 0;
 }
 
 .profile-entry__form-submit {
diff --git a/src/avatar-selector.html b/src/avatar-selector.html
new file mode 100644
index 0000000000000000000000000000000000000000..8496ef94d7f95ec90d29ae5c7e3c030c15659238
--- /dev/null
+++ b/src/avatar-selector.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<html>
+
+<head>
+  <meta charset="utf-8">
+  <link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
+  <% if(NODE_ENV === "production") { %>
+    <script src="https://cdn.rawgit.com/brianpeiris/aframe/845825ae694449524c185c44a314d361eead4680/dist/aframe-master.min.js"></script>
+  <% } else { %>
+    <script src="https://cdn.rawgit.com/brianpeiris/aframe/845825ae694449524c185c44a314d361eead4680/dist/aframe-master.js"></script>
+  <% } %>
+</head>
+
+<body>
+  <div id="selector-root"></div>
+</body>
+
+</html>
diff --git a/src/avatar-selector.js b/src/avatar-selector.js
new file mode 100644
index 0000000000000000000000000000000000000000..55ac8df6f8aff0d76a6e9fff96a922a3cd21b652
--- /dev/null
+++ b/src/avatar-selector.js
@@ -0,0 +1,45 @@
+import ReactDOM from "react-dom";
+import React from "react";
+import queryString from "query-string";
+
+import "./assets/stylesheets/avatar-selector.scss";
+import "./vendor/GLTFLoader";
+
+import "./components/animation-mixer";
+import "./components/audio-feedback";
+import "./components/loop-animation";
+import "./elements/a-progressive-asset";
+import "./gltf-component-mappings";
+import { avatars } from "./assets/avatars/avatars.json";
+import { avatarIds } from "./utils/identity";
+
+import { App } from "./App";
+import AvatarSelector from "./react-components/avatar-selector";
+
+window.APP = new App();
+const hash = queryString.parse(location.hash);
+const isMobile = AFRAME.utils.device.isMobile();
+if (hash.quality) {
+  window.APP.quality = hash.quality;
+} else {
+  window.APP.quality = isMobile ? "low" : "high";
+}
+
+const avatar = hash.avatar;
+
+function postAvatarToParent(newAvatar) {
+  window.parent.postMessage({avatar: newAvatar}, location.origin);
+}
+
+function mountUI() {
+  const selector = ReactDOM.render(
+    <AvatarSelector {...{ avatars, avatar, onChange: postAvatarToParent }} />,
+    document.getElementById("selector-root")
+  );
+
+  window.addEventListener('hashchange', () => {
+    const hash = queryString.parse(location.hash);
+    selector.setState({avatar: hash.avatar});
+  });
+}
+document.addEventListener("DOMContentLoaded", mountUI);
diff --git a/src/components/debug.js b/src/components/debug.js
new file mode 100644
index 0000000000000000000000000000000000000000..77202f03295be677b9156e1ff16f11100ab19351
--- /dev/null
+++ b/src/components/debug.js
@@ -0,0 +1,25 @@
+AFRAME.registerComponent("lifecycle-checker", {
+  schema: {
+    tick: { default: false }
+  },
+  init: function() {
+    console.log("init", this.el);
+  },
+  update: function() {
+    console.log("update", this.el);
+  },
+  tick: function() {
+    if (this.data.tick) {
+      console.log("tick", this.el);
+    }
+  },
+  remove: function() {
+    console.log("remove", this.el);
+  },
+  pause: function() {
+    console.log("pause", this.el);
+  },
+  play: function() {
+    console.log("play", this.el);
+  }
+});
diff --git a/src/components/ik-controller.js b/src/components/ik-controller.js
index 3b4f323df72d83cfbe0d03ab00094318485eed22..ea8a1d216ad19cefc1b7d7450330888e1776c9c6 100644
--- a/src/components/ik-controller.js
+++ b/src/components/ik-controller.js
@@ -23,13 +23,16 @@ AFRAME.registerComponent("ik-root", {
       this.rightController = this.el.querySelector(this.data.rightController);
       updated = true;
     }
-
-    if (updated) {
-      this.el.querySelector("[ik-controller]").components["ik-controller"].updateIkRoot(this);
-    }
   }
 });
 
+function findIKRoot(entity) {
+  while (entity && !(entity.components && entity.components["ik-root"])) {
+    entity = entity.parentNode;
+  }
+  return entity && entity.components["ik-root"];
+}
+
 AFRAME.registerComponent("ik-controller", {
   schema: {
     leftEye: { type: "string", default: ".LeftEye" },
@@ -65,6 +68,8 @@ AFRAME.registerComponent("ik-controller", {
     this.rootToChest = new Matrix4();
     this.invRootToChest = new Matrix4();
 
+    this.ikRoot = findIKRoot(this.el);
+
     this.hands = {
       left: {
         lastVisible: true,
@@ -124,10 +129,6 @@ AFRAME.registerComponent("ik-controller", {
       .negate();
   },
 
-  updateIkRoot(ikRoot) {
-    this.ikRoot = ikRoot;
-  },
-
   tick(time, dt) {
     if (!this.ikRoot) {
       return;
diff --git a/src/components/player-info.js b/src/components/player-info.js
new file mode 100644
index 0000000000000000000000000000000000000000..717b1d2a96336f38ced2ce511b68b12f8acdcddf
--- /dev/null
+++ b/src/components/player-info.js
@@ -0,0 +1,32 @@
+AFRAME.registerComponent("player-info", {
+  schema: {
+    displayName: { type: "string" },
+    avatar: { type: "string" }
+  },
+  init() {
+    this.applyProperties = this.applyProperties.bind(this);
+  },
+  play() {
+    this.el.addEventListener("model-loaded", this.applyProperties);
+  },
+  pause() {
+    this.el.removeEventListener("model-loaded", this.applyProperties);
+  },
+  update(oldProps) {
+    this.applyProperties();
+  },
+  applyProperties() {
+    const nametagEl = this.el.querySelector(".nametag");
+    console.log("updating properties", this.data, nametagEl);
+    if (this.data.displayName && nametagEl) {
+      nametagEl.setAttribute("text", {
+        value: this.data.displayName
+      });
+    }
+
+    const modelEl = this.el.querySelector(".model");
+    if (this.data.avatar && modelEl) {
+      modelEl.setAttribute("src", this.data.avatar);
+    }
+  }
+});
diff --git a/src/elements/a-gltf-entity.js b/src/elements/a-gltf-entity.js
index 3c9c07031eecbf21069569b0f2738fe48afcea25..2c36e2fc0409053c73ac0b0fe8f14a22fd2f50cc 100644
--- a/src/elements/a-gltf-entity.js
+++ b/src/elements/a-gltf-entity.js
@@ -2,6 +2,9 @@ const GLTFCache = {};
 
 AFRAME.AGLTFEntity = {
   defaultInflator(el, componentName, componentData) {
+    if (!AFRAME.components[componentName]) {
+      throw new Error(`Inflator failed. "${componentName}" component does not exist.`);
+    }
     if (AFRAME.components[componentName].multiple && Array.isArray(componentData)) {
       for (let i = 0; i < componentData.length; i++) {
         el.setAttribute(componentName + "__" + i, componentData[i]);
@@ -22,19 +25,21 @@ AFRAME.AGLTFEntity = {
 // From https://gist.github.com/cdata/f2d7a6ccdec071839bc1954c32595e87
 // Tracking glTF cloning here: https://github.com/mrdoob/three.js/issues/11573
 function cloneGltf(gltf) {
-  const clone = {
-    animations: gltf.animations,
-    scene: gltf.scene.clone(true)
-  };
-
   const skinnedMeshes = {};
-
   gltf.scene.traverse(node => {
+    if (!node.name) {
+      node.name = node.uuid;
+    }
     if (node.isSkinnedMesh) {
       skinnedMeshes[node.name] = node;
     }
   });
 
+  const clone = {
+    animations: gltf.animations,
+    scene: gltf.scene.clone(true)
+  };
+
   const cloneBones = {};
   const cloneSkinnedMeshes = {};
 
@@ -68,17 +73,22 @@ function cloneGltf(gltf) {
   return clone;
 }
 
-const inflateEntities = function(classPrefix, parentEl, node) {
+const inflateEntities = function(parentEl, node) {
   // setObject3D mutates the node's parent, so we have to copy
   const children = node.children.slice(0);
 
   const el = document.createElement("a-entity");
 
   // Remove invalid CSS class name characters.
-  const className = node.name.replace(/[^\w-]/g, "");
-  el.classList.add(classPrefix + className);
+  const className = (node.name || node.uuid).replace(/[^\w-]/g, "");
+  el.classList.add(className);
   parentEl.appendChild(el);
 
+  // AFRAME rotation component expects rotations in YXZ, convert it
+  if (node.rotation.order !== "YXZ") {
+    node.rotation.setFromQuaternion(node.quaternion, "YXZ");
+  }
+
   // Copy over transform to the THREE.Group and reset the actual transform of the Object3D
   el.setAttribute("position", {
     x: node.position.x,
@@ -96,9 +106,8 @@ const inflateEntities = function(classPrefix, parentEl, node) {
     z: node.scale.z
   });
 
-  node.position.set(0, 0, 0);
-  node.rotation.set(0, 0, 0);
-  node.scale.set(1, 1, 1);
+  node.matrixAutoUpdate = false;
+  node.matrix.identity();
 
   el.setObject3D(node.type.toLowerCase(), node);
 
@@ -116,7 +125,6 @@ const inflateEntities = function(classPrefix, parentEl, node) {
   }
 
   const entityComponents = node.userData.components;
-
   if (entityComponents) {
     for (const prop in entityComponents) {
       if (entityComponents.hasOwnProperty(prop)) {
@@ -130,116 +138,164 @@ const inflateEntities = function(classPrefix, parentEl, node) {
   }
 
   children.forEach(childNode => {
-    inflateEntities(classPrefix, el, childNode);
+    inflateEntities(el, childNode);
   });
-};
 
-function attachTemplate(templateEl) {
-  const selector = templateEl.getAttribute("data-selector");
-  const targetEls = templateEl.parentNode.querySelectorAll(selector);
-  const clone = document.importNode(templateEl.content, true);
-  const templateRoot = clone.firstElementChild;
+  return el;
+};
 
+function attachTemplate(root, { selector, templateRoot }) {
+  const targetEls = root.querySelectorAll(selector);
   for (const el of targetEls) {
+    const root = templateRoot.cloneNode(true);
     // Merge root element attributes with the target element
-    for (const { name, value } of templateRoot.attributes) {
+    for (const { name, value } of root.attributes) {
       el.setAttribute(name, value);
     }
 
     // Append all child elements
-    for (const child of templateRoot.children) {
-      el.appendChild(document.importNode(child, true));
+    for (const child of root.children) {
+      el.appendChild(child);
     }
   }
 }
 
+function cachedLoadGLTF(src, onProgress) {
+  return new Promise((resolve, reject) => {
+    // Load the gltf model from the cache if it exists.
+    if (GLTFCache[src]) {
+      // Use a cloned copy of the cached model.
+      resolve(cloneGltf(GLTFCache[src]));
+    } else {
+      // Otherwise load the new gltf model.
+      new THREE.GLTFLoader().load(
+        src,
+        model => {
+          if (!GLTFCache[src]) {
+            // Store a cloned copy of the gltf model.
+            GLTFCache[src] = cloneGltf(model);
+          }
+          resolve(model);
+        },
+        onProgress,
+        reject
+      );
+    }
+  });
+}
+
 AFRAME.registerElement("a-gltf-entity", {
   prototype: Object.create(AFRAME.AEntity.prototype, {
     load: {
-      value() {
+      async value() {
         if (this.hasLoaded || !this.parentEl) {
           return;
         }
 
-        // Get the src url.
-        let src = this.getAttribute("src");
+        // The code above and below this are from AEntity.prototype.load, we need to monkeypatch in gltf loading mid function
+        await this.loadTemplates();
+        await this.setSrc(this.getAttribute("src"));
+        //
 
-        // If the src attribute is a selector, get the url from the asset item.
-        if (src.charAt(0) === "#") {
-          const assetEl = document.getElementById(src.substring(1));
-
-          const fallbackSrc = assetEl.getAttribute("src");
-          const highSrc = assetEl.getAttribute("high-src");
-          const lowSrc = assetEl.getAttribute("low-src");
+        AFRAME.ANode.prototype.load.call(this, () => {
+          // Check if entity was detached while it was waiting to load.
+          if (!this.parentEl) {
+            return;
+          }
 
-          if (highSrc && window.APP.quality === "high") {
-            src = highSrc;
-          } else if (lowSrc && window.APP.quality === "low") {
-            src = lowSrc;
-          } else {
-            src = fallbackSrc;
+          this.updateComponents();
+          if (this.isScene || this.parentEl.isPlaying) {
+            this.play();
           }
-        }
+        });
+      }
+    },
 
-        const onLoad = gltfModel => {
-          if (!GLTFCache[src]) {
-            // Store a cloned copy of the gltf model.
-            GLTFCache[src] = cloneGltf(gltfModel);
+    loadTemplates: {
+      value() {
+        return new Promise((resolve, reject) => {
+          this.templates = [];
+          this.querySelectorAll(":scope > template").forEach(templateEl => {
+            this.templates.push({
+              selector: templateEl.getAttribute("data-selector"),
+              templateRoot: document.importNode(templateEl.firstElementChild || templateEl.content.firstElementChild, true)
+            })
+          });
+          setTimeout(resolve, 0);
+        });
+      }
+    },
+
+    setSrc: {
+      async value(src) {
+        try {
+          // If the src attribute is a selector, get the url from the asset item.
+          if (src && src.charAt(0) === "#") {
+            const assetEl = document.getElementById(src.substring(1));
+            if (!assetEl) { return; }
+
+            const fallbackSrc = assetEl.getAttribute("src");
+            const highSrc = assetEl.getAttribute("high-src");
+            const lowSrc = assetEl.getAttribute("low-src");
+
+            if (highSrc && window.APP.quality === "high") {
+              src = highSrc;
+            } else if (lowSrc && window.APP.quality === "low") {
+              src = lowSrc;
+            } else {
+              src = fallbackSrc;
+            }
           }
 
-          this.model = gltfModel.scene || gltfModel.scenes[0];
-          this.model.animations = gltfModel.animations;
+          if (src === this.lastSrc) return;
+          this.lastSrc = src;
 
-          this.setObject3D("mesh", this.model);
-          this.emit("model-loaded", { format: "gltf", model: this.model });
+          if (!src) return;
 
-          if (this.getAttribute("inflate")) {
-            inflate(this.model, finalizeLoad);
-          } else {
-            finalizeLoad();
-          }
-        };
+          const model = await cachedLoadGLTF(src);
 
-        const inflate = (model, callback) => {
-          inflateEntities("", this, model);
-          this.querySelectorAll(":scope > template").forEach(attachTemplate);
+          // If we started loading something else already
+          // TODO: there should be a way to cancel loading instead
+          if (src != this.lastSrc) return;
 
-          // Wait one tick for the appended custom elements to be connected before calling finalizeLoad
-          setTimeout(callback, 0);
-        };
+          // If we had inflated something already before, clean that up
+          if (this.inflatedEl) {
+            this.inflatedEl.parentNode.removeChild(this.inflatedEl);
+            delete this.inflatedEl;
+          }
 
-        const finalizeLoad = () => {
-          AFRAME.ANode.prototype.load.call(this, () => {
-            // Check if entity was detached while it was waiting to load.
-            if (!this.parentEl) {
-              return;
-            }
+          this.model = model.scene || model.scenes[0];
+          this.model.animations = model.animations;
 
-            this.updateComponents();
-            if (this.isScene || this.parentEl.isPlaying) {
-              this.play();
-            }
-          });
-        };
+          this.setObject3D("mesh", this.model);
 
-        // Load the gltf model from the cache if it exists.
-        const gltf = GLTFCache[src];
+          if (this.getAttribute("inflate")) {
+            this.inflatedEl = inflateEntities(this, this.model);
+            this.templates.forEach(attachTemplate.bind(null, this));
+          }
 
-        if (gltf) {
-          // Use a cloned copy of the cached model.
-          const clonedGltf = cloneGltf(gltf);
-          onLoad(clonedGltf);
-          return;
+          this.emit("model-loaded", { format: "gltf", model: this.model });
+        } catch (e) {
+          console.error("Failed to load glTF model", e.message, this);
+          this.emit("model-error", { format: "gltf", src });
         }
+      }
+    },
 
-        // Otherwise load the new gltf model.
-        new THREE.GLTFLoader().load(src, onLoad, undefined /* onProgress */, error => {
-          // On glTF load error
+    attributeChangedCallback: {
+      value(attr, oldVal, newVal) {
+        if (attr === "src") {
+          this.setSrc(newVal);
+        }
+      }
+    },
 
-          const message = error && error.message ? error.message : "Failed to load glTF model";
-          console.warn(message);
-          this.emit("model-error", { format: "gltf", src });
-        });
+    setAttribute: {
+      value(attr, arg1, arg2) {
+        if (attr === "src") {
+          this.setSrc(arg1);
+        }
+        AFRAME.AEntity.prototype.setAttribute.call(this, attr, arg1, arg2);
       }
     }
   })
diff --git a/src/elements/a-progressive-asset.js b/src/elements/a-progressive-asset.js
index a367e374aece30163bc73802ab8cf11950f28c1a..8bf922aa17b10ce6eec09baf413e93dcf6d871f1 100644
--- a/src/elements/a-progressive-asset.js
+++ b/src/elements/a-progressive-asset.js
@@ -9,17 +9,17 @@ AFRAME.registerElement("a-progressive-asset", {
       value() {
         this.data = null;
         this.isAssetItem = true;
+      }
+    },
 
+    attachedCallback: {
+      value() {
         if (!this.parentNode.fileLoader) {
           throw new Error("a-progressive-asset must be the child of an a-assets element.");
         }
 
         this.fileLoader = this.parentNode.fileLoader;
-      }
-    },
 
-    attachedCallback: {
-      value() {
         const self = this;
         const fallbackSrc = this.getAttribute("src");
         const highSrc = this.getAttribute("high-src");
diff --git a/src/network-schemas.js b/src/network-schemas.js
index 4632a5933807b1e97132c726fcc335afa6b68964..54cc0b63ba40efed17baf43c0d3554b1ea2fa4bc 100644
--- a/src/network-schemas.js
+++ b/src/network-schemas.js
@@ -5,6 +5,7 @@ function registerNetworkSchemas() {
       "position",
       "rotation",
       "scale",
+      "player-info",
       {
         selector: ".camera",
         component: "position"
@@ -36,11 +37,6 @@ function registerNetworkSchemas() {
       {
         selector: ".right-controller",
         component: "visible"
-      },
-      {
-        selector: ".nametag",
-        component: "text",
-        property: "value"
       }
     ]
   });
diff --git a/src/react-components/avatar-selector.js b/src/react-components/avatar-selector.js
new file mode 100644
index 0000000000000000000000000000000000000000..203f855ad5c2ad8b022b34a7bde23f836fab7038
--- /dev/null
+++ b/src/react-components/avatar-selector.js
@@ -0,0 +1,98 @@
+import React, { Component } from 'react';
+import PropTypes from 'prop-types';
+
+class AvatarSelector extends Component {
+  static propTypes = {
+    avatars: PropTypes.array,
+    avatar: PropTypes.string,
+    onChange: PropTypes.func,
+  }
+
+  constructor(props) {
+    super(props);
+    this.state = { avatar: this.props.avatar };
+  }
+
+  getAvatarIndex(direction=0) {
+    const currAvatarIndex = this.props.avatars.findIndex(avatar => avatar.id === this.state.avatar);
+    const numAvatars = this.props.avatars.length;
+    return ((currAvatarIndex + direction) % numAvatars + numAvatars) % numAvatars;
+  }
+
+  nextAvatar = () => {
+    const newAvatarIndex = this.getAvatarIndex(1);
+    this.props.onChange(this.props.avatars[newAvatarIndex].id);
+  }
+
+  prevAvatar = () => {
+    const newAvatarIndex = this.getAvatarIndex(-1);
+    this.props.onChange(this.props.avatars[newAvatarIndex].id);
+  }
+
+  render () {
+    const avatarAssets = this.props.avatars.map(avatar => (
+      <a-progressive-asset
+        id={avatar.id}
+        key={avatar.id}
+        response-type="arraybuffer"
+        high-src={`./src/assets/avatars/${avatar.models.high}`}
+        low-src={`./src/assets/avatars/${avatar.models.low}`}
+      ></a-progressive-asset>
+    ));
+
+    const avatarEntities = this.props.avatars.map((avatar, i) => (
+      <a-entity key={avatar.id} position="0 0 0" rotation={`0 ${360 * i / this.props.avatars.length} 0`}>
+        <a-gltf-entity position="0 0 5" rotation="0 180 0" src={'#' + avatar.id} inflate="true">
+          <template data-selector=".RootScene">
+            <a-entity animation-mixer></a-entity>
+          </template>
+          <a-animation attribute="rotation" dur="2000" to="0 -180 0" fill="forwards" repeat="indefinite"></a-animation>
+        </a-gltf-entity>
+      </a-entity>
+    ));
+
+    return (
+      <div className="avatar-selector">
+      <a-scene vr-mode-ui="enabled: false" debug>
+        <a-assets>
+          {avatarAssets}
+          <a-asset-item 
+            id="meeting-space1-mesh" 
+            response-type="arraybuffer" 
+            src="./src/assets/environments/MeetingSpace1_mesh.glb"
+          ></a-asset-item>
+        </a-assets>
+
+        <a-entity rotation={`0 ${360 * this.getAvatarIndex() / this.props.avatars.length - 180} 0`}>
+        {avatarEntities}
+        </a-entity>
+
+        <a-entity position="0 1.5 -6.6" rotation="-10 180 0" camera></a-entity>
+
+        <a-entity
+          hide-when-quality="low"
+          light="type: directional; color: #F9FFCE; intensity: 0.6"
+          position="0 5 -15"
+        ></a-entity>
+        <a-entity 
+          hide-when-quality="low"
+          light="type: ambient; color: #FFF"
+        ></a-entity>
+        <a-gltf-entity
+          id="meeting-space"
+          src="#meeting-space1-mesh"
+          position="0 0 0"
+        ></a-gltf-entity>
+      </a-scene>
+      <button className="avatar-selector__prev-button" onClick={this.nextAvatar}>
+        <i className="avatar-selector__button-icon material-icons">keyboard_arrow_left</i>
+      </button>
+      <button className="avatar-selector__next-button" onClick={this.prevAvatar}>
+        <i className="avatar-selector__button-icon material-icons">keyboard_arrow_right</i>
+      </button>
+      </div>
+    );
+  }
+}
+
+export default AvatarSelector;
diff --git a/src/react-components/profile-entry-panel.js b/src/react-components/profile-entry-panel.js
index 28c8cd7d9ce8c8bf9902f6a654c835bbd7fe95ea..45721138607615d4ded41ecc4fcced174df9b20d 100644
--- a/src/react-components/profile-entry-panel.js
+++ b/src/react-components/profile-entry-panel.js
@@ -13,29 +13,49 @@ class ProfileEntryPanel extends Component {
   constructor(props) {
     super(props);
     window.store = this.props.store;
-    this.state = {name: this.props.store.state.profile.display_name};
+    this.state = { 
+      display_name: this.props.store.state.profile.display_name,
+      avatar: this.props.store.state.profile.avatar,
+    };
     this.props.store.addEventListener("statechanged", this.storeUpdated);
   }
 
   storeUpdated = () => {
-    this.setState({name: this.props.store.state.profile.display_name});
+    this.setState({ 
+      display_name: this.props.store.state.profile.display_name,
+      avatar: this.props.store.state.profile.avatar,
+    });
   }
 
-  saveName = (e) => {
+  saveStateAndFinish = (e) => {
     e.preventDefault();
-    this.props.store.update({ profile: { display_name: this.nameInput.value } });
+    this.props.store.update({profile: { 
+      display_name: this.state.display_name,
+      avatar: this.state.avatar
+    }});
     this.props.finished();
   }
 
+  stopPropagation = (e) => {
+    e.stopPropagation();
+  }
+
   componentDidMount() {
     // stop propagation so that avatar doesn't move when wasd'ing during text input.
-    this.nameInput.addEventListener('keydown', e => e.stopPropagation());
-    this.nameInput.addEventListener('keypress', e => e.stopPropagation());
-    this.nameInput.addEventListener('keyup', e => e.stopPropagation());
+    this.nameInput.addEventListener('keydown', this.stopPropagation);
+    this.nameInput.addEventListener('keypress', this.stopPropagation);
+    this.nameInput.addEventListener('keyup', this.stopPropagation);
+    window.addEventListener('message', (e) => {
+      if (e.source !== this.avatarSelector.contentWindow) { return; }
+      this.setState({avatar: e.data.avatar});
+    });
   }
   
   componentWillUnmount() {
     this.props.store.removeEventListener('statechanged', this.storeUpdated);
+    this.nameInput.removeEventListener('keydown', this.stopPropagation);
+    this.nameInput.removeEventListener('keypress', this.stopPropagation);
+    this.nameInput.removeEventListener('keyup', this.stopPropagation);
   }
 
   render () {
@@ -43,19 +63,23 @@ class ProfileEntryPanel extends Component {
 
     return (
       <div className="profile-entry">
-        <form onSubmit={this.saveName}>
+        <form onSubmit={this.saveStateAndFinish}>
         <div className="profile-entry__box profile-entry__box--darkened">
           <div className="profile-entry__subtitle">
             <FormattedMessage id="profile.header"/>
           </div>
           <input
             className="profile-entry__form-field-text"
-            value={this.state.name} onChange={(e) => this.setState({name: e.target.value})}
+            value={this.state.display_name} onChange={(e) => this.setState({display_name: e.target.value})}
             required pattern={SCHEMA.definitions.profile.properties.display_name.pattern}
             title={formatMessage({ id: "profile.display_name.validation_warning" })}
             ref={inp => this.nameInput = inp}/>
+          <iframe 
+            className="profile-entry__avatar-selector" 
+            src={`avatar-selector.html#avatar=${this.state.avatar}`}
+            ref={ifr => this.avatarSelector = ifr}>loading...</iframe>
           <input className="profile-entry__form-submit" type="submit" value={formatMessage({ id: "profile.save" }) }/>
-          </div>
+        </div>
         </form>
       </div>
     );
diff --git a/src/react-components/ui-root.js b/src/react-components/ui-root.js
index e2f4c2a1629def812e484f2617b07d3c61548d2a..9a7118ad4cfcae9badbaa238621f2124444d39c8 100644
--- a/src/react-components/ui-root.js
+++ b/src/react-components/ui-root.js
@@ -90,7 +90,7 @@ class UIRoot extends Component {
     sceneLoaded: false,
     exited: false,
 
-    showProfileEntry: false
+    showProfileEntry: true
   }
 
   componentDidMount() {
diff --git a/src/room.html b/src/room.html
index 52fd9ddd2a9ab2cdc4b54dba0f0093c55dee0d7f..0bc4d32f480927adc71b3d6fee8f06ed1891a60b 100644
--- a/src/room.html
+++ b/src/room.html
@@ -30,7 +30,7 @@
                 high-src="./assets/avatars/BotDefault_Avatar.glb"
                 low-src="./assets/avatars/BotDefault_Avatar_Unlit.glb"
             ></a-progressive-asset>
-            
+            <a-asset-item id="bot-dom-mesh" response-type="arraybuffer" src="./assets/avatars/BotDom_Avatar.glb"></a-asset-item>
             <a-asset-item id="watch-model" response-type="arraybuffer" src="./assets/hud/watch.glb"></a-asset-item>
 
             <a-asset-item id="meeting-space1-mesh" response-type="arraybuffer" src="./assets/environments/MeetingSpace1_mesh.glb"></a-asset-item>
@@ -48,16 +48,16 @@
             </template>
 
             <template id="remote-avatar-template">
-                <a-entity ik-root>
+                <a-entity ik-root player-info>
                     <a-entity class="camera"></a-entity>
 
                     <a-entity class="left-controller"></a-entity>
 
                     <a-entity class="right-controller"></a-entity>
 
-                    <a-gltf-entity src="#bot-skinned-mesh" inflate="true" ik-controller >
+                    <a-gltf-entity class="model" inflate="true">
                         <template data-selector=".RootScene">
-                            <a-entity animation-mixer ></a-entity>
+                            <a-entity ik-controller animation-mixer></a-entity>
                         </template>
 
                         <template data-selector=".Neck">
@@ -81,7 +81,7 @@
                             </a-entity>
                         </template>
 
-                        <template selector=".LeftHand">
+                        <template data-selector=".LeftHand">
                             <a-entity personal-space-invader ></a-entity>
                         </template>
 
@@ -147,6 +147,7 @@
             wasd-to-analog2d
             character-controller="pivot: #player-camera"
             ik-root
+            player-info
         >
             <a-entity
                 id="player-camera"
@@ -181,9 +182,13 @@
                 haptic-feedback
             ></a-entity>
 
-            <a-gltf-entity src="#bot-skinned-mesh" inflate="true" ik-controller >
+            <a-gltf-entity class="model" inflate="true">
                 <template data-selector=".RootScene">
-                    <a-entity animation-mixer animated-robot-hands ></a-entity>
+                    <a-entity
+                        ik-controller 
+                        animated-robot-hands
+                        animation-mixer
+                    ></a-entity>
                 </template>
 
                 <template data-selector=".Neck">
diff --git a/src/room.js b/src/room.js
index b09ff896a09145e54bb76ea45f9f659606eaa022..48a33fea05c6a0218cc3761a4a6f1fd64d0bbb02 100644
--- a/src/room.js
+++ b/src/room.js
@@ -37,6 +37,8 @@ import "./components/layers";
 import "./components/spawn-controller";
 import "./components/animated-robot-hands";
 import "./components/hide-when-quality";
+import "./components/player-info";
+import "./components/debug";
 import "./components/animation-mixer";
 import "./components/loop-animation";
 
@@ -128,9 +130,11 @@ async function exitScene() {
   document.body.removeChild(scene);
 }
 
-function setNameTagFromStore() {
-  const myNametag = document.querySelector("#player-rig .nametag");
-  myNametag.setAttribute("text", "value", store.state.profile.display_name);
+function applyProfile(playerRig) {
+  playerRig.setAttribute("player-info", {
+    displayName: store.state.profile.display_name,
+    avatar: store.state.profile.avatar || "#bot-skinned-mesh"
+  });
 }
 
 async function enterScene(mediaStream, enterInVR) {
@@ -148,6 +152,7 @@ async function enterScene(mediaStream, enterInVR) {
   document.querySelector("#player-camera").setAttribute("look-controls", "pointerLockEnabled: true;");
 
   const qs = queryString.parse(location.search);
+  const playerRig = document.querySelector("#player-rig");
 
   scene.setAttribute("networked-scene", {
     room: qs.room && !isNaN(parseInt(qs.room)) ? parseInt(qs.room) : 1,
@@ -159,12 +164,12 @@ async function enterScene(mediaStream, enterInVR) {
   }
 
   if (isMobile || qs.mobile) {
-    const playerRig = document.querySelector("#player-rig");
     playerRig.setAttribute("virtual-gamepad-controls", {});
   }
 
-  setNameTagFromStore();
-  store.addEventListener('statechanged', setNameTagFromStore);
+  const applyProfileOnPlayerRig = applyProfile.bind(null, playerRig);
+  applyProfileOnPlayerRig();
+  store.addEventListener("statechanged", applyProfileOnPlayerRig);
 
   const avatarScale = parseInt(qs.avatarScale, 10);
 
diff --git a/src/storage/store.js b/src/storage/store.js
index 5b00b7ac8f5570c28651587997ae4c0546a3b722..7e7d1a722e7aa53c581b7fe1498a132d7e7257e5 100644
--- a/src/storage/store.js
+++ b/src/storage/store.js
@@ -17,6 +17,7 @@ export const SCHEMA = {
       additionalProperties: false,
       properties: {
         display_name: { type: "string", pattern: "^[A-Za-z0-9-]{3,32}$" },
+        avatar: { type: "string" },
       }
     }
   },
diff --git a/src/utils/identity.js b/src/utils/identity.js
index 72cabdf008bc2574b4408d62c8f7f17f30fc9a59..b1d5148251d76b637b9ce0201d5f2b7e28fbcff2 100644
--- a/src/utils/identity.js
+++ b/src/utils/identity.js
@@ -1,3 +1,5 @@
+import { avatars } from "../assets/avatars/avatars.json";
+
 const names = [
   "albattani",
   "allen",
@@ -161,7 +163,16 @@ const names = [
   "yonath"
 ];
 
+function selectRandom(arr) {
+   return arr[Math.floor(Math.random() * arr.length)]
+}
+
+export const avatarIds = avatars.map(av => av.id);
+
 export function generateDefaultProfile() {
-  const name = names[Math.floor(Math.random() * names.length)];
-  return { display_name: name.replace(/^./, name[0].toUpperCase()) };
+  const name = selectRandom(names);
+  return {
+    display_name: name.replace(/^./, name[0].toUpperCase()) ,
+    avatar: selectRandom(avatarIds)
+  };
 }
diff --git a/webpack.config.js b/webpack.config.js
index 4996b8925731f3e5912b954dafa5c6b1dc2a66c2..d404babbb6d5af438271621b4e3f2fbac235a266 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -77,6 +77,7 @@ const config = {
   entry: {
     lobby: path.join(__dirname, "src", "lobby.js"),
     room: path.join(__dirname, "src", "room.js"),
+    'avatar-selector': path.join(__dirname, "src", "avatar-selector.js"),
     onboarding: path.join(__dirname, "src", "onboarding.js")
   },
   output: {
@@ -196,6 +197,12 @@ const config = {
       chunks: ["room"],
       inject: "head"
     }),
+    new HTMLWebpackPlugin({
+      filename: "avatar-selector.html",
+      template: path.join(__dirname, "src", "avatar-selector.html"),
+      chunks: ["avatar-selector"],
+      inject: "head"
+    }),
     new HTMLWebpackPlugin({
       filename: "onboarding.html",
       template: path.join(__dirname, "src", "onboarding.html"),