diff --git a/assignment-client/src/avatars/MixerAvatar.cpp b/assignment-client/src/avatars/MixerAvatar.cpp
index ac633d9388..5203d25af6 100644
--- a/assignment-client/src/avatars/MixerAvatar.cpp
+++ b/assignment-client/src/avatars/MixerAvatar.cpp
@@ -32,24 +32,42 @@ MixerAvatar::MixerAvatar() {
 
     _challengeTimer.setSingleShot(true);
     _challengeTimer.setInterval(CHALLENGE_TIMEOUT_MS);
-    
-    _challengeTimer.callOnTimeout(this, [this]() {
-        if (_verifyState == challengeClient) {
-            _pendingEvent = false;
-            _verifyState = verificationFailed;
-            _needsIdentityUpdate = true;
-            qCDebug(avatars) << "Dynamic verification TIMED-OUT for" << getDisplayName() << getSessionUUID();
-        } else {
-            qCDebug(avatars) << "Ignoring timeout of avatar challenge";
-        }
-    });
-
+    _challengeTimer.callOnTimeout(this, &MixerAvatar::challengeTimeout);
+    // QTimer::start is a set of overloaded functions.
+    connect(this, &MixerAvatar::startChallengeTimer, &_challengeTimer, static_cast<void(QTimer::*)()>(&QTimer::start));
 }
 
 const char* MixerAvatar::stateToName(VerifyState state) {
     return QMetaEnum::fromType<VerifyState>().valueToKey(state);
 }
 
+void MixerAvatar::challengeTimeout() {
+    switch (_verifyState) {
+    case challengeClient:
+        _verifyState = staticValidation;
+        _pendingEvent = true;
+        if (++_numberChallenges < NUM_CHALLENGES_BEFORE_FAIL) {
+            qCDebug(avatars) << "Retrying (" << _numberChallenges << ") timed-out challenge for" << getDisplayName()
+                << getSessionUUID();
+        } else {
+            _certifyFailed = true;
+            _needsIdentityUpdate = true;
+            qCWarning(avatars) << "ALERT: Dynamic verification TIMED-OUT for" << getDisplayName() << getSessionUUID();
+        }
+        break;
+
+    case verificationFailed:
+        qCDebug(avatars)  << "Retrying failed challenge for" << getDisplayName() << getSessionUUID();
+        _verifyState = staticValidation;
+        _pendingEvent = true;
+        break;
+
+    default:
+        qCDebug(avatars) << "Ignoring timeout of avatar challenge";
+        break;
+    }
+}
+
 void MixerAvatar::fetchAvatarFST() {
     if (_verifyState >= requestingFST && _verifyState <= challengeClient) {
         qCDebug(avatars) << "WARNING: Avatar verification restarted; old state:" << stateToName(_verifyState);
@@ -58,7 +76,7 @@ void MixerAvatar::fetchAvatarFST() {
 
     _pendingEvent = false;
 
-    QUrl avatarURL = getSkeletonModelURL();
+    QUrl avatarURL = _skeletonModelURL;
     if (avatarURL.isEmpty() || avatarURL.isLocalFile() || avatarURL.scheme() == "qrc") {
         // Not network FST.
         return;
@@ -210,6 +228,23 @@ void MixerAvatar::ownerRequestComplete() {
     networkReply->deleteLater();
 }
 
+void MixerAvatar::requestCurrentOwnership() {
+    // Get registered owner's public key from metaverse.
+    static const QString POP_MARKETPLACE_API { "/api/v1/commerce/proof_of_purchase_status/transfer" };
+    auto& networkAccessManager = NetworkAccessManager::getInstance();
+    QNetworkRequest networkRequest;
+    networkRequest.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true);
+    networkRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
+    QUrl requestURL = NetworkingConstants::METAVERSE_SERVER_URL();
+    requestURL.setPath(POP_MARKETPLACE_API);
+    networkRequest.setUrl(requestURL);
+
+    QJsonObject request;
+    request["certificate_id"] = _certificateIdFromFST;
+    QNetworkReply* networkReply = networkAccessManager.put(networkRequest, QJsonDocument(request).toJson());
+    connect(networkReply, &QNetworkReply::finished, this, &MixerAvatar::ownerRequestComplete);
+}
+
 void MixerAvatar::processCertifyEvents() {
     if (!_pendingEvent) {
         return;
@@ -221,26 +256,15 @@ void MixerAvatar::processCertifyEvents() {
         case receivedFST:
         {
             generateFSTHash();
+            _numberChallenges = 0;
             if (_certificateIdFromFST.length() != 0) {
                 QString& marketplacePublicKey = EntityItem::_marketplacePublicKey;
                 bool staticVerification = validateFSTHash(marketplacePublicKey);
                 _verifyState = staticVerification ? staticValidation : verificationFailed;
 
                 if (_verifyState == staticValidation) {
-                    static const QString POP_MARKETPLACE_API { "/api/v1/commerce/proof_of_purchase_status/transfer" };
-                    auto& networkAccessManager = NetworkAccessManager::getInstance();
-                    QNetworkRequest networkRequest;
-                    networkRequest.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true);
-                    networkRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
-                    QUrl requestURL = NetworkingConstants::METAVERSE_SERVER_URL();
-                    requestURL.setPath(POP_MARKETPLACE_API);
-                    networkRequest.setUrl(requestURL);
-
-                    QJsonObject request;
-                    request["certificate_id"] = _certificateIdFromFST;
+                    requestCurrentOwnership();
                     _verifyState = requestingOwner;
-                    QNetworkReply* networkReply = networkAccessManager.put(networkRequest, QJsonDocument(request).toJson());
-                    connect(networkReply, &QNetworkReply::finished, this, &MixerAvatar::ownerRequestComplete);
                 } else {
                     _needsIdentityUpdate = true;
                     _pendingEvent = false;
@@ -248,11 +272,21 @@ void MixerAvatar::processCertifyEvents() {
                 }
             } else {  // FST doesn't have a certificate, so noncertified rather than failed:
                 _pendingEvent = false;
+                _certifyFailed = false;
+                _needsIdentityUpdate = true;
                 _verifyState = nonCertified;
+                qCDebug(avatars) << "Avatar " << getDisplayName() << "(" << getSessionUUID() << ") isn't certified";
             }
             break;
         }
 
+        case staticValidation:
+        {
+            requestCurrentOwnership();
+            _verifyState = requestingOwner;
+            break;
+        }
+
         case ownerResponse:
         {
             QJsonDocument responseJson = QJsonDocument::fromJson(_dynamicMarketResponse.toUtf8());
@@ -310,23 +344,28 @@ void MixerAvatar::processCertifyEvents() {
 void MixerAvatar::sendOwnerChallenge() {
     auto nodeList = DependencyManager::get<NodeList>();
     QByteArray avatarID = ("{" + _marketplaceIdFromFST + "}").toUtf8();
-    QByteArray nonce = QUuid::createUuid().toByteArray();
+    if (_challengeNonce.isEmpty()) {
+        _challengeNonce = QUuid::createUuid().toByteArray();
+        QCryptographicHash nonceHash(QCryptographicHash::Sha256);
+        nonceHash.addData(_challengeNonce);
+        _challengeNonceHash = nonceHash.result();
+
+    }
 
     auto challengeOwnershipPacket = NLPacket::create(PacketType::ChallengeOwnership,
-        2 * sizeof(int) + nonce.length() + avatarID.length(), true);
+        2 * sizeof(int) + _challengeNonce.length() + avatarID.length(), true);
     challengeOwnershipPacket->writePrimitive(avatarID.length());
-    challengeOwnershipPacket->writePrimitive(nonce.length());
+    challengeOwnershipPacket->writePrimitive(_challengeNonce.length());
     challengeOwnershipPacket->write(avatarID);
-    challengeOwnershipPacket->write(nonce);
+    challengeOwnershipPacket->write(_challengeNonce);
 
     nodeList->sendPacket(std::move(challengeOwnershipPacket), *(nodeList->nodeWithUUID(getSessionUUID())) );
     QCryptographicHash nonceHash(QCryptographicHash::Sha256);
-    nonceHash.addData(nonce);
+    nonceHash.addData(_challengeNonce);
     _challengeNonceHash = nonceHash.result();
     _pendingEvent = false;
     
-    // QTimer::start is a set of overloaded functions.
-    QMetaObject::invokeMethod(&_challengeTimer, static_cast<void(QTimer::*)()>(&QTimer::start));
+    emit startChallengeTimer();
 }
 
 void MixerAvatar::processChallengeResponse(ReceivedMessage& response) {
@@ -337,7 +376,7 @@ void MixerAvatar::processChallengeResponse(ReceivedMessage& response) {
         QByteArray responseData = response.readAll();
         if (responseData.length() < 8) {
             _verifyState = error;
-            qCDebug(avatars) << "Avatar challenge response packet too small, length:" << responseData.length();
+            qCWarning(avatars) << "ALERT: Avatar challenge response packet too small, length:" << responseData.length();
             return;
         }
 
@@ -354,11 +393,14 @@ void MixerAvatar::processChallengeResponse(ReceivedMessage& response) {
         bool challengeResult = EntityItemProperties::verifySignature(_ownerPublicKey, _challengeNonceHash,
             QByteArray::fromBase64(signedNonce));
         _verifyState = challengeResult ? verificationSucceeded : verificationFailed;
+        _certifyFailed = !challengeResult;
         _needsIdentityUpdate = true;
-        if (_verifyState == verificationFailed) {
+        if (_certifyFailed) {
             qCDebug(avatars) << "Dynamic verification FAILED for" << getDisplayName() << getSessionUUID();
+            emit startChallengeTimer();
         } else {
             qCDebug(avatars) << "Dynamic verification SUCCEEDED for" << getDisplayName() << getSessionUUID();
+            _challengeNonce.clear();
         }
 
     } else {
diff --git a/assignment-client/src/avatars/MixerAvatar.h b/assignment-client/src/avatars/MixerAvatar.h
index 39095def50..ec24d4e6bc 100644
--- a/assignment-client/src/avatars/MixerAvatar.h
+++ b/assignment-client/src/avatars/MixerAvatar.h
@@ -27,7 +27,7 @@ public:
     void setNeedsHeroCheck(bool needsHeroCheck = true) { _needsHeroCheck = needsHeroCheck; }
 
     void fetchAvatarFST();
-    virtual bool isCertifyFailed() const override { return _verifyState == verificationFailed; }
+    virtual bool isCertifyFailed() const override { return _certifyFailed; }
     bool needsIdentityUpdate() const { return _needsIdentityUpdate; }
     void setNeedsIdentityUpdate(bool value = true) { _needsIdentityUpdate = value; }
 
@@ -58,13 +58,18 @@ private:
     QString _certificateIdFromFST;
     QString _dynamicMarketResponse;
     QString _ownerPublicKey;
+    QByteArray _challengeNonce;
     QByteArray _challengeNonceHash;
     QTimer _challengeTimer;
+    static constexpr int NUM_CHALLENGES_BEFORE_FAIL = 1;
+    int _numberChallenges { 0 };
+    bool _certifyFailed { false };
     bool _needsIdentityUpdate { false };
 
     bool generateFSTHash();
     bool validateFSTHash(const QString& publicKey) const;
     QByteArray canonicalJson(const QString fstFile);
+    void requestCurrentOwnership();
     void sendOwnerChallenge();
 
     static const QString VERIFY_FAIL_MODEL;
@@ -72,6 +77,10 @@ private:
 private slots:
     void fstRequestComplete();
     void ownerRequestComplete();
+    void challengeTimeout();
+
+ signals:
+    void startChallengeTimer();
 };
 
 using MixerAvatarSharedPointer = std::shared_ptr<MixerAvatar>;