diff --git a/src/claims/payloads/claimLinkIdentity.ts b/src/claims/payloads/claimLinkIdentity.ts
index b0394db45..095f19724 100644
--- a/src/claims/payloads/claimLinkIdentity.ts
+++ b/src/claims/payloads/claimLinkIdentity.ts
@@ -1,5 +1,9 @@
 import type { Claim, SignedClaim } from '../types';
-import type { NodeIdEncoded, ProviderIdentityIdEncoded } from '../../ids/types';
+import type {
+  NodeIdEncoded,
+  ProviderIdentityClaimId,
+  ProviderIdentityIdEncoded,
+} from '../../ids/types';
 import * as ids from '../../ids';
 import * as claimsUtils from '../utils';
 import * as tokensUtils from '../../tokens/utils';
@@ -13,6 +17,7 @@ interface ClaimLinkIdentity extends Claim {
   typ: 'ClaimLinkIdentity';
   iss: NodeIdEncoded;
   sub: ProviderIdentityIdEncoded;
+  providerIdentityClaimId?: ProviderIdentityClaimId;
 }
 
 function assertClaimLinkIdentity(
diff --git a/src/discovery/Discovery.ts b/src/discovery/Discovery.ts
index 50a61acf0..3ec2e4af0 100644
--- a/src/discovery/Discovery.ts
+++ b/src/discovery/Discovery.ts
@@ -13,9 +13,11 @@ import type IdentitiesManager from '../identities/IdentitiesManager';
 import type {
   IdentityData,
   IdentityId,
+  IdentitySignedClaim,
   ProviderId,
   ProviderIdentityClaimId,
   ProviderIdentityId,
+  ProviderPaginationToken,
 } from '../identities/types';
 import type KeyRing from '../keys/KeyRing';
 import type { ClaimIdEncoded, SignedClaim } from '../claims/types';
@@ -165,10 +167,8 @@ class Discovery {
         }),
       );
     } catch (e) {
-      if (
-        e instanceof tasksErrors.ErrorTaskStop ||
-        e === discoveryStoppingTaskReason
-      ) {
+      // We need to reschedule if the task was cancelled due to discovery domain stopping
+      if (e === discoveryStoppingTaskReason) {
         // We need to recreate the task for the vertex
         const vertexId = gestaltsUtils.decodeGestaltId(vertex);
         if (vertexId == null) never();
@@ -177,6 +177,7 @@ class Discovery {
           undefined,
           undefined,
           gestaltsUtils.decodeGestaltId(parent ?? undefined),
+          true,
         );
         return;
       }
@@ -276,6 +277,10 @@ class Discovery {
       this.discoverVertexHandlerId,
       this.discoverVertexHandler,
     );
+    this.taskManager.registerHandler(
+      this.checkRediscoveryHandlerId,
+      this.checkRediscoveryHandler,
+    );
     // Start up rediscovery task
     await this.taskManager.scheduleTask({
       handlerId: this.discoverVertexHandlerId,
@@ -303,6 +308,7 @@ class Discovery {
     }
     await Promise.all(taskPromises);
     this.taskManager.deregisterHandler(this.discoverVertexHandlerId);
+    this.taskManager.deregisterHandler(this.checkRediscoveryHandlerId);
     this.logger.info(`Stopped ${this.constructor.name}`);
   }
 
@@ -450,14 +456,14 @@ class Discovery {
       switch (signedClaim.payload.typ) {
         case 'ClaimLinkNode':
           await this.processClaimLinkNode(
-            signedClaim,
+            signedClaim as SignedClaim<ClaimLinkNode>,
             nodeId,
             lastProcessedCutoffTime,
           );
           break;
         case 'ClaimLinkIdentity':
           await this.processClaimLinkIdentity(
-            signedClaim,
+            signedClaim as SignedClaim<ClaimLinkIdentity>,
             nodeId,
             ctx,
             lastProcessedCutoffTime,
@@ -474,7 +480,7 @@ class Discovery {
   }
 
   protected async processClaimLinkNode(
-    signedClaim: SignedClaim,
+    signedClaim: SignedClaim<ClaimLinkNode>,
     nodeId: NodeId,
     lastProcessedCutoffTime = Date.now() - this.rediscoverSkipTime,
   ): Promise<void> {
@@ -498,9 +504,9 @@ class Discovery {
       );
       return;
     }
-    const linkedVertexNodeId = node1Id.equals(nodeId) ? node2Id : node1Id;
+    const linkedNodeId = node1Id.equals(nodeId) ? node2Id : node1Id;
     const linkedVertexNodeInfo: GestaltNodeInfo = {
-      nodeId: linkedVertexNodeId,
+      nodeId: linkedNodeId,
     };
     await this.gestaltGraph.linkNodeAndNode(
       {
@@ -508,7 +514,7 @@ class Discovery {
       },
       linkedVertexNodeInfo,
       {
-        claim: signedClaim as SignedClaim<ClaimLinkNode>,
+        claim: signedClaim,
         meta: {},
       },
     );
@@ -516,7 +522,7 @@ class Discovery {
     if (claimId == null) never();
     await this.gestaltGraph.setClaimIdNewest(nodeId, claimId);
     // Add this vertex to the queue if it hasn't already been visited
-    const linkedGestaltId: GestaltId = ['node', linkedVertexNodeId];
+    const linkedGestaltId: GestaltId = ['node', linkedNodeId];
     if (
       !(await this.processedTimeGreaterThan(
         linkedGestaltId,
@@ -533,7 +539,7 @@ class Discovery {
   }
 
   protected async processClaimLinkIdentity(
-    signedClaim: SignedClaim,
+    signedClaim: SignedClaim<ClaimLinkIdentity>,
     nodeId: NodeId,
     ctx: ContextTimed,
     lastProcessedCutoffTime = Date.now() - this.rediscoverSkipTime,
@@ -565,19 +571,8 @@ class Discovery {
       return;
     }
     // Need to get the corresponding claim for this
-    let providerIdentityClaimId: ProviderIdentityClaimId | null = null;
-    const identityClaims = await this.verifyIdentityClaims(
-      providerId,
-      identityId,
-    );
-    for (const [id, claim] of Object.entries(identityClaims)) {
-      const issuerNodeId = nodesUtils.decodeNodeId(claim.payload.iss);
-      if (issuerNodeId == null) continue;
-      if (nodeId.equals(issuerNodeId)) {
-        providerIdentityClaimId = id as ProviderIdentityClaimId;
-        break;
-      }
-    }
+    const providerIdentityClaimId = signedClaim.payload
+      .providerIdentityClaimId as ProviderIdentityClaimId | null;
     if (providerIdentityClaimId == null) {
       this.logger.warn(
         `Failed to get corresponding identity claim for ${providerId}:${identityId}`,
@@ -591,7 +586,7 @@ class Discovery {
       },
       identityInfo,
       {
-        claim: signedClaim as SignedClaim<ClaimLinkIdentity>,
+        claim: signedClaim,
         meta: {
           providerIdentityClaimId: providerIdentityClaimId,
           url: identityInfo.url,
@@ -633,27 +628,41 @@ class Discovery {
       identityId,
       ctx,
     );
+    let lastProviderPaginationToken = await this.gestaltGraph
+      .getIdentity(providerIdentityId)
+      .then((identity) => identity?.lastProviderPaginationToken);
     // If we don't have identity info, simply skip this vertex
     if (vertexIdentityInfo == null) {
       return;
     }
     // Getting and verifying claims
-    const claims = await this.verifyIdentityClaims(providerId, identityId);
-    // Link the identity with each node from its claims on the provider
-    // Iterate over each of the claims
-    for (const [claimId, claim] of Object.entries(claims)) {
-      if (ctx.signal.aborted) throw ctx.signal.reason;
+    const {
+      identityClaims,
+      lastProviderPaginationToken: lastProviderPaginationToken_,
+    } = await this.verifyIdentityClaims(
+      providerId,
+      identityId,
+      lastProviderPaginationToken,
+      ctx,
+    );
+    const isAborted = ctx.signal.aborted;
+    lastProviderPaginationToken = lastProviderPaginationToken_;
+    // Iterate over each of the claims, even if ctx has aborted
+    for (const [claimId, claim] of Object.entries(identityClaims)) {
       // Claims on an identity provider will always be node -> identity
       // So just cast payload data as such
-      const linkedVertexNodeId = nodesUtils.decodeNodeId(claim.payload.iss);
-      if (linkedVertexNodeId == null) never();
+      const linkedNodeId = nodesUtils.decodeNodeId(claim.payload.iss);
+      if (linkedNodeId == null) never();
       // With this verified chain, we can link
       const linkedVertexNodeInfo = {
-        nodeId: linkedVertexNodeId,
+        nodeId: linkedNodeId,
       };
       await this.gestaltGraph.linkNodeAndIdentity(
         linkedVertexNodeInfo,
-        vertexIdentityInfo,
+        {
+          ...vertexIdentityInfo,
+          lastProviderPaginationToken,
+        },
         {
           claim: claim,
           meta: {
@@ -662,8 +671,8 @@ class Discovery {
           },
         },
       );
-      // Add this vertex to the queue if it is not present
-      const gestaltNodeId: GestaltId = ['node', linkedVertexNodeId];
+      // Check and schedule node for processing
+      const gestaltNodeId: GestaltId = ['node', linkedNodeId];
       if (
         !(await this.processedTimeGreaterThan(
           gestaltNodeId,
@@ -678,6 +687,11 @@ class Discovery {
         );
       }
     }
+    // Throw after we have processed the node claims if the signal aborted whilst running verifyIdentityClaims
+    if (isAborted) {
+      throw ctx.signal.reason;
+    }
+    // Only setVertexProcessedTime if we have succeeded in processing all identities
     await this.gestaltGraph.setVertexProcessedTime(
       ['identity', providerIdentityId],
       Date.now(),
@@ -732,6 +746,7 @@ class Discovery {
     delay?: number,
     lastProcessedCutoffTime?: number,
     parent?: GestaltId,
+    ignoreActive: boolean = false,
     tran?: DBTransaction,
   ) {
     if (tran == null) {
@@ -741,6 +756,7 @@ class Discovery {
           delay,
           lastProcessedCutoffTime,
           parent,
+          ignoreActive,
           tran,
         ),
       );
@@ -762,6 +778,8 @@ class Discovery {
       [this.constructor.name, this.discoverVertexHandlerId, gestaltIdEncoded],
       tran,
     )) {
+      // Ignore active tasks
+      if (ignoreActive && task.status === 'active') continue;
       if (taskExisting == null) {
         taskExisting = task;
         continue;
@@ -845,33 +863,43 @@ class Discovery {
    * Helper function to retrieve and verify the claims of an identity on a given
    * provider. Connects with each node the identity claims to be linked with,
    * and verifies the claim with the public key of the node.
+   *
+   * This method never throws if ctx has aborted, opting instead to return early
+   * with a lastProviderPaginationToken so that the caller can process the partially
+   * requested Claims as well as resume progress when calling again.
    */
   protected async verifyIdentityClaims(
     providerId: ProviderId,
     identityId: IdentityId,
-  ): Promise<Record<ProviderIdentityClaimId, SignedClaim<ClaimLinkIdentity>>> {
+    providerPaginationToken: ProviderPaginationToken | undefined,
+    ctx: ContextTimed,
+  ): Promise<{
+    identityClaims: Record<
+      ProviderIdentityClaimId,
+      SignedClaim<ClaimLinkIdentity>
+    >;
+    lastProviderPaginationToken?: ProviderPaginationToken;
+  }> {
     const provider = this.identitiesManager.getProvider(providerId);
     // If we don't have this provider, no identity info to find
     if (provider == null) {
-      return {};
+      return { identityClaims: {} };
     }
     // Get our own auth identity id
     const authIdentityIds = await provider.getAuthIdentityIds();
     // If we don't have one then we can't request data so just skip
     if (authIdentityIds.length === 0 || authIdentityIds[0] == null) {
-      return {};
+      return { identityClaims: {} };
     }
     const authIdentityId = authIdentityIds[0];
     const identityClaims: Record<
       ProviderIdentityClaimId,
       SignedClaim<ClaimLinkIdentity>
     > = {};
-    for await (const identitySignedClaim of provider.getClaims(
-      authIdentityId,
-      identityId,
-    )) {
-      identitySignedClaim.claim;
-      // Claims on an identity provider will always be node -> identity
+
+    const identitySignedClaimDb = (
+      identitySignedClaim: IdentitySignedClaim,
+    ) => {
       const claim = identitySignedClaim.claim;
       const data = claim.payload;
       // Verify the claim with the public key of the node
@@ -883,8 +911,75 @@ class Discovery {
       if (token.verifyWithPublicKey(publicKey)) {
         identityClaims[identitySignedClaim.id] = claim;
       }
-    }
-    return identityClaims;
+    };
+    let nextPaginationToken: ProviderPaginationToken | undefined =
+      providerPaginationToken;
+    let processed: boolean;
+    do {
+      // Refresh before each request made with identitySignedClaimGenerator
+      ctx.timer.refresh();
+      processed = false;
+      if (provider.preferGetClaimsPage) {
+        const iterator = provider.getClaimIdsPage(
+          authIdentityId,
+          identityId,
+          nextPaginationToken,
+        );
+        for await (const wrapper of iterator) {
+          processed = true;
+          // This will:
+          // 1. throw if the getClaimIdsPage takes too much time
+          // 2. the rest of this loop iteration takes too much time
+          if (ctx.signal.aborted) {
+            return {
+              identityClaims: identityClaims,
+              lastProviderPaginationToken: nextPaginationToken,
+            };
+          }
+          const claimId = wrapper.claimId;
+          if (wrapper.nextPaginationToken != null) {
+            nextPaginationToken = wrapper.nextPaginationToken;
+          }
+          // Refresh timer in preparation for request
+          ctx.timer.refresh();
+          const identitySignedClaim = await provider.getClaim(
+            authIdentityId,
+            claimId,
+          );
+          if (identitySignedClaim == null) {
+            continue;
+          }
+          // Claims on an identity provider will always be node -> identity
+          identitySignedClaimDb(identitySignedClaim);
+        }
+      } else {
+        const iterator = provider.getClaimsPage(
+          authIdentityId,
+          identityId,
+          nextPaginationToken,
+        );
+        for await (const wrapper of iterator) {
+          processed = true;
+          // This will:
+          // 1. throw if the getClaimIdsPage takes too much time
+          // 2. the rest of this loop iteration takes too much time
+          if (ctx.signal.aborted) {
+            return {
+              identityClaims: identityClaims,
+              lastProviderPaginationToken: nextPaginationToken,
+            };
+          }
+          if (wrapper.nextPaginationToken != null) {
+            nextPaginationToken = wrapper.nextPaginationToken;
+          }
+          // Claims on an identity provider will always be node -> identity
+          identitySignedClaimDb(wrapper.claim);
+        }
+      }
+    } while (processed);
+    return {
+      identityClaims,
+    };
   }
 
   /**
diff --git a/src/gestalts/types.ts b/src/gestalts/types.ts
index 294b6e30c..1cfb039f2 100644
--- a/src/gestalts/types.ts
+++ b/src/gestalts/types.ts
@@ -9,6 +9,7 @@ import type {
 } from '../ids/types';
 import type { SignedClaim, SignedClaimJSON } from '../claims/types';
 import type { ClaimLinkIdentity, ClaimLinkNode } from '../claims/payloads';
+import type { ProviderPaginationToken } from '../identities/types';
 
 const gestaltActions = ['notify', 'scan', 'claim'] as const;
 
@@ -41,6 +42,11 @@ type GestaltIdentityInfo = {
   name?: string;
   email?: string;
   url?: string;
+  /**
+   * Used to determine the last pagination token to resume
+   * from when discovering the identity vertex
+   */
+  lastProviderPaginationToken?: ProviderPaginationToken;
   // The `undefined` is a hack to include the optional reserved properties
   [key: string]: JSONValue | undefined;
 };
diff --git a/src/identities/IdentitiesManager.ts b/src/identities/IdentitiesManager.ts
index c5aa731b6..1f8c6d5f3 100644
--- a/src/identities/IdentitiesManager.ts
+++ b/src/identities/IdentitiesManager.ts
@@ -7,7 +7,6 @@ import type {
 } from './types';
 import type { DB, DBTransaction, KeyPath, LevelPath } from '@matrixai/db';
 import type Provider from './Provider';
-import type { SignedClaim } from '../claims/types';
 import type { ClaimLinkIdentity } from '../claims/payloads';
 import type KeyRing from '../keys/KeyRing';
 import type Sigchain from '../sigchain/Sigchain';
@@ -19,6 +18,7 @@ import {
 import Logger from '@matrixai/logger';
 import * as identitiesErrors from './errors';
 import * as identitiesEvents from './events';
+import Token from '../tokens/Token';
 import * as nodesUtils from '../nodes/utils';
 import { promise } from '../utils';
 import { encodeProviderIdentityId } from '../ids';
@@ -274,15 +274,22 @@ class IdentitiesManager {
           sub: encodeProviderIdentityId([providerId, identityId]),
         },
         undefined,
-        async (token) => {
+        async (token: Token<ClaimLinkIdentity>) => {
           // Publishing in the callback to avoid adding bad claims
           const claim = token.toSigned();
           const identitySignedClaim = await provider.publishClaim(
             identityId,
-            claim as SignedClaim<ClaimLinkIdentity>,
+            claim,
           );
           publishedClaimProm.resolveP(identitySignedClaim);
-          return token;
+          // Append the ProviderIdentityClaimId to the token
+          const payload: ClaimLinkIdentity = {
+            ...claim.payload,
+            providerIdentityClaimId: identitySignedClaim.id,
+          };
+          const newToken = Token.fromPayload(payload);
+          newToken.signWithPrivateKey(this.keyRing.keyPair);
+          return newToken;
         },
         tran,
       ),
diff --git a/src/identities/Provider.ts b/src/identities/Provider.ts
index ce3f29ae4..be0dbec6f 100644
--- a/src/identities/Provider.ts
+++ b/src/identities/Provider.ts
@@ -7,6 +7,7 @@ import type {
   ProviderTokens,
   ProviderAuthenticateRequest,
   ProviderIdentityClaimId,
+  ProviderPaginationToken,
 } from './types';
 import type { SignedClaim } from '../claims/types';
 import type { ClaimLinkIdentity } from '../claims/payloads/claimLinkIdentity';
@@ -28,6 +29,14 @@ abstract class Provider {
    */
   public abstract readonly id: ProviderId;
 
+  /**
+   * Set to true if getClaimsPage method should be preferred instead claim iteration operations.
+   * This could be useful if the Provider subclass has a getClaimsPage implentation that is able to
+   * obtain both Claims and ClaimsIds with a single HTTP request. For example, if the Provider were to
+   * supply a GraphQL API, or if the webscraped page were to contain the contents of both.
+   */
+  public readonly preferGetClaimsPage: boolean = false;
+
   public getTokens: GetTokens;
   public getToken: GetToken;
   public putToken: PutToken;
@@ -187,13 +196,95 @@ abstract class Provider {
     claimId: ProviderIdentityClaimId,
   ): Promise<IdentitySignedClaim | undefined>;
 
+  /**
+   * Stream a page of identity claimIds from an identity
+   */
+  public abstract getClaimIdsPage(
+    authIdentityId: IdentityId,
+    identityId: IdentityId,
+    paginationToken?: ProviderPaginationToken,
+  ): AsyncGenerator<{
+    claimId: ProviderIdentityClaimId;
+    nextPaginationToken?: ProviderPaginationToken;
+  }>;
+
+  /**
+   * Stream identity claimIds from an identity
+   */
+  public async *getClaimIds(
+    authIdentityId: IdentityId,
+    identityId: IdentityId,
+  ): AsyncGenerator<ProviderIdentityClaimId> {
+    let nextPaginationToken: ProviderPaginationToken | undefined;
+    while (true) {
+      const iterator = this.getClaimIdsPage(
+        authIdentityId,
+        identityId,
+        nextPaginationToken,
+      );
+      nextPaginationToken = undefined;
+      for await (const wrapper of iterator) {
+        nextPaginationToken = wrapper.nextPaginationToken;
+        yield wrapper.claimId;
+      }
+      if (nextPaginationToken == null) {
+        break;
+      }
+    }
+  }
+
+  /**
+   * Stream a page of identity claims from an identity
+   */
+  public async *getClaimsPage(
+    authIdentityId: IdentityId,
+    identityId: IdentityId,
+    paginationToken?: ProviderPaginationToken,
+  ): AsyncGenerator<{
+    claim: IdentitySignedClaim;
+    nextPaginationToken?: ProviderPaginationToken;
+  }> {
+    const iterator = this.getClaimIdsPage(
+      authIdentityId,
+      identityId,
+      paginationToken,
+    );
+    for await (const { claimId, nextPaginationToken } of iterator) {
+      const claim = await this.getClaim(authIdentityId, claimId);
+      if (claim == null) {
+        continue;
+      }
+      yield {
+        claim,
+        nextPaginationToken,
+      };
+    }
+  }
+
   /**
    * Stream identity claims from an identity
    */
-  public abstract getClaims(
+  public async *getClaims(
     authIdentityId: IdentityId,
     identityId: IdentityId,
-  ): AsyncGenerator<IdentitySignedClaim>;
+  ): AsyncGenerator<IdentitySignedClaim> {
+    let nextPaginationToken: ProviderPaginationToken | undefined;
+    while (true) {
+      const iterator = this.getClaimsPage(
+        authIdentityId,
+        identityId,
+        nextPaginationToken,
+      );
+      nextPaginationToken = undefined;
+      for await (const wrapper of iterator) {
+        nextPaginationToken = wrapper.nextPaginationToken;
+        yield wrapper.claim;
+      }
+      if (nextPaginationToken == null) {
+        break;
+      }
+    }
+  }
 }
 
 export default Provider;
diff --git a/src/identities/providers/github/GitHubProvider.ts b/src/identities/providers/github/GitHubProvider.ts
index d3e413069..c40e87fd2 100644
--- a/src/identities/providers/github/GitHubProvider.ts
+++ b/src/identities/providers/github/GitHubProvider.ts
@@ -6,6 +6,7 @@ import type {
   IdentitySignedClaim,
   ProviderIdentityClaimId,
   ProviderAuthenticateRequest,
+  ProviderPaginationToken,
 } from '../../types';
 import type { SignedClaim } from '../../../claims/types';
 import type { ClaimLinkIdentity } from '../../../claims/payloads/claimLinkIdentity';
@@ -21,12 +22,14 @@ import * as utils from '../../../utils';
 class GitHubProvider extends Provider {
   public readonly id = 'github.com' as ProviderId;
   public readonly clientId: string;
+  public readonly preferGetClaimsPage: boolean = true;
 
   protected readonly apiUrl: string = 'https://api.github.com';
   protected readonly gistFilename: string = 'cryptolink.txt';
   protected readonly gistDescription: string =
     'Cryptolink between Polykey Keynode and Github Identity';
   protected readonly scope: string = 'gist user:email read:user';
+  protected readonly pageSize: number = 10; // GitHub Gists has a maximum page size, we can use this to figure out if there are anymore pages
   protected logger: Logger;
 
   public constructor({
@@ -518,41 +521,88 @@ class GitHubProvider extends Provider {
   }
 
   /**
-   * Gets all IdentitySignedClaims from a given identity.
+   * Gets a page of ProviderIdentityClaimIds from a given identity
+   * sorted by latest.
    */
-  public async *getClaims(
-    authIdentityId: IdentityId,
+  public async *getClaimIdsPage(
+    _authIdentityId: IdentityId,
     identityId: IdentityId,
-  ): AsyncGenerator<IdentitySignedClaim> {
+    paginationToken?: ProviderPaginationToken,
+  ): AsyncGenerator<{
+    claimId: ProviderIdentityClaimId;
+    nextPaginationToken?: ProviderPaginationToken;
+  }> {
     const gistsSearchUrl = 'https://gist.github.com/search';
-    let pageNum = 1;
-    while (true) {
-      const url = new URL(gistsSearchUrl);
-      url.searchParams.set('p', pageNum.toString());
-      url.searchParams.set(
-        'q',
-        `user:${identityId} filename:${this.gistFilename} ${this.gistDescription}`, // Githubidentityclaim
+    const url = new URL(gistsSearchUrl);
+    let query = `user:${identityId} filename:${this.gistFilename} ${this.gistDescription}`;
+    if (paginationToken != null) {
+      query += ` updated:<${paginationToken}`;
+    }
+    url.searchParams.set('q', query);
+    url.searchParams.set('s', 'updated'); // Sort by updated
+    url.searchParams.set('o', 'desc'); // Sort by descending
+    const request = new Request(url.toString(), { method: 'GET' });
+    const response = await fetch(request);
+    if (!response.ok) {
+      throw new identitiesErrors.ErrorProviderCall(
+        `Provider responded with ${response.status} ${response.statusText}`,
       );
-      const request = new Request(url.toString(), { method: 'GET' });
-      const response = await fetch(request);
-      if (!response.ok) {
-        throw new identitiesErrors.ErrorProviderCall(
-          `Provider responded with ${response.status} ${response.statusText}`,
-        );
-      }
-      const data = await response.text();
-      const claimIds = this.extractClaimIds(data);
-      for (const claimId of claimIds) {
-        const claim = await this.getClaim(authIdentityId, claimId);
-        if (claim != null) {
-          yield claim;
-        }
-      }
-      if (claimIds.length === 0) {
-        break;
-      } else {
-        pageNum = pageNum + 1;
-      }
+    }
+    const data = await response.text();
+    const claimIds = this.extractClaimIds(data);
+    let newPaginationToken: ProviderPaginationToken | undefined;
+    if (claimIds.length >= this.pageSize) {
+      newPaginationToken = this.extractPaginationToken(data);
+    }
+    for (const [i, claimId] of claimIds.entries()) {
+      yield {
+        claimId,
+        nextPaginationToken:
+          i === claimIds.length - 1 ? newPaginationToken : undefined,
+      };
+    }
+  }
+
+  /**
+   * Gets all IdentitySignedClaims from a given identity
+   * sorted by latest.
+   */
+  public async *getClaimsPage(
+    _authIdentityId: IdentityId,
+    identityId: IdentityId,
+    paginationToken?: ProviderPaginationToken,
+  ): AsyncGenerator<{
+    claim: IdentitySignedClaim;
+    nextPaginationToken?: ProviderPaginationToken;
+  }> {
+    const gistsSearchUrl = 'https://gist.github.com/search';
+    const url = new URL(gistsSearchUrl);
+    let query = `user:${identityId} filename:${this.gistFilename} ${this.gistDescription}`;
+    if (paginationToken != null) {
+      query += ` updated:<${paginationToken}`;
+    }
+    url.searchParams.set('q', query);
+    url.searchParams.set('s', 'updated'); // Sort by updated
+    url.searchParams.set('o', 'desc'); // Sort by descending
+    const request = new Request(url.toString(), { method: 'GET' });
+    const response = await fetch(request);
+    if (!response.ok) {
+      throw new identitiesErrors.ErrorProviderCall(
+        `Provider responded with ${response.status} ${response.statusText}`,
+      );
+    }
+    const data = await response.text();
+    const claims = this.extractClaims(data);
+    let newPaginationToken: ProviderPaginationToken | undefined;
+    if (claims.length >= this.pageSize) {
+      newPaginationToken = this.extractPaginationToken(data);
+    }
+    for (const [i, claim] of claims.entries()) {
+      yield {
+        claim,
+        nextPaginationToken:
+          i === claims.length - 1 ? newPaginationToken : undefined,
+      };
     }
   }
 
@@ -573,6 +623,15 @@ class GitHubProvider extends Provider {
     }) as Request;
   }
 
+  protected extractPaginationToken(
+    html: string,
+  ): ProviderPaginationToken | undefined {
+    const $ = cheerio.load(html);
+    return $('.gist-snippet > .gist-snippet-meta relative-time')
+      .last()
+      .attr('datetime') as ProviderPaginationToken | undefined;
+  }
+
   protected extractClaimIds(html: string): Array<ProviderIdentityClaimId> {
     const claimIds: Array<ProviderIdentityClaimId> = [];
     const $ = cheerio.load(html);
@@ -590,6 +649,29 @@ class GitHubProvider extends Provider {
       });
     return claimIds;
   }
+
+  protected extractClaims(html: string): Array<IdentitySignedClaim> {
+    const claims: Array<IdentitySignedClaim> = [];
+    const $ = cheerio.load(html);
+    $('.gist-snippet').each((_, ele) => {
+      const claimId = $('.gist-snippet-meta > ul > li > a', ele)
+        .first()
+        .attr('href')
+        ?.match(/\/.+?\/(.+)/)
+        ?.at(1);
+      const signedClaim = this.parseClaim($(`.blob-code-inner`, ele).text());
+      if (claimId == null || signedClaim == null) {
+        return;
+      }
+      claims.push({
+        id: claimId as ProviderIdentityClaimId,
+        url: `https://gist.github.com/${claimId}`,
+        claim: signedClaim,
+      });
+    });
+
+    return claims;
+  }
 }
 
 export default GitHubProvider;
diff --git a/src/identities/types.ts b/src/identities/types.ts
index 0106bdb54..f96133f7b 100644
--- a/src/identities/types.ts
+++ b/src/identities/types.ts
@@ -1,4 +1,4 @@
-import type { POJO } from '../types';
+import type { POJO, Opaque } from '../types';
 import type {
   ProviderId,
   IdentityId,
@@ -54,12 +54,21 @@ type ProviderAuthenticateRequest = {
   data: POJO;
 };
 
+/**
+ * Generic opaque string used to represent a pagination token
+ * when making requests using an identity Provider.
+ * Do not expect this string to have any particular meaning
+ * outside of the Provider subclass it is used for.
+ */
+type ProviderPaginationToken = Opaque<'ProviderPaginationToken', string>;
+
 export type {
   IdentityData,
   IdentitySignedClaim,
   ProviderToken,
   ProviderTokens,
   ProviderAuthenticateRequest,
+  ProviderPaginationToken,
 };
 
 export type {
diff --git a/tests/client/handlers/gestalts.test.ts b/tests/client/handlers/gestalts.test.ts
index f4a2b3131..c2d4311bd 100644
--- a/tests/client/handlers/gestalts.test.ts
+++ b/tests/client/handlers/gestalts.test.ts
@@ -1472,13 +1472,28 @@ describe('gestaltsGestaltTrustByIdentity', () => {
       iss: nodesUtils.encodeNodeId(keyRing.getNodeId()),
       sub: encodeProviderIdentityId([testProvider.id, connectedIdentity]),
     };
-    const [claimId_, claim] = await sigchain.addClaim(identityClaim);
+    const [claimId_, claim] = await sigchain.addClaim(
+      identityClaim,
+      undefined,
+      async (token: Token<ClaimLinkIdentity>) => {
+        // Publishing in the callback to avoid adding bad claims
+        const claim = token.toSigned();
+        const identitySignedClaim = await testProvider.publishClaim(
+          connectedIdentity,
+          claim,
+        );
+        // Append the ProviderIdentityClaimId to the token
+        const payload: ClaimLinkIdentity = {
+          ...claim.payload,
+          providerIdentityClaimId: identitySignedClaim.id,
+        };
+        const newToken = Token.fromPayload(payload);
+        newToken.signWithPrivateKey(keyRing.keyPair);
+        return newToken;
+      },
+    );
     claimId = claimId_;
     nodeChainData[claimId_] = claim;
-    await testProvider.publishClaim(
-      connectedIdentity,
-      claim as SignedClaim<ClaimLinkIdentity>,
-    );
     clientService = new ClientService({
       tlsConfig,
       logger: logger.getChild(ClientService.name),
@@ -1747,12 +1762,27 @@ describe('gestaltsGestaltTrustByNode', () => {
       iss: nodeIdEncodedRemote,
       sub: encodeProviderIdentityId([testProvider.id, connectedIdentity]),
     };
-    const [claimId, claim] = await node.sigchain.addClaim(identityClaim);
-    nodeChainData[claimId] = claim;
-    await testProvider.publishClaim(
-      connectedIdentity,
-      claim as SignedClaim<ClaimLinkIdentity>,
+    const [claimId, claim] = await node.sigchain.addClaim(
+      identityClaim,
+      undefined,
+      async (token: Token<ClaimLinkIdentity>) => {
+        // Publishing in the callback to avoid adding bad claims
+        const claim = token.toSigned();
+        const identitySignedClaim = await testProvider.publishClaim(
+          connectedIdentity,
+          claim,
+        );
+        // Append the ProviderIdentityClaimId to the token
+        const payload: ClaimLinkIdentity = {
+          ...claim.payload,
+          providerIdentityClaimId: identitySignedClaim.id,
+        };
+        const newToken = Token.fromPayload(payload);
+        newToken.signWithPrivateKey(node.keyRing.keyPair);
+        return newToken;
+      },
     );
+    nodeChainData[claimId] = claim;
 
     const keysPath = path.join(dataDir, 'keys');
     const dbPath = path.join(dataDir, 'db');
diff --git a/tests/discovery/Discovery.test.ts b/tests/discovery/Discovery.test.ts
index 289dd658d..e5c9ef2ad 100644
--- a/tests/discovery/Discovery.test.ts
+++ b/tests/discovery/Discovery.test.ts
@@ -1,11 +1,10 @@
 import type { IdentityId, ProviderId } from '@/identities/types';
 import type { Host } from '@/network/types';
 import type { Key } from '@/keys/types';
-import type { SignedClaim } from '../../src/claims/types';
-import type { ClaimLinkIdentity } from '@/claims/payloads';
-import type { NodeId } from '../../src/ids';
+import type { NodeId } from '@/ids';
 import type { AgentServerManifest } from '@/nodes/agent/handlers';
 import type { DiscoveryQueueInfo } from '@/discovery/types';
+import type { ClaimLinkIdentity } from '@/claims/payloads/claimLinkIdentity';
 import fs from 'fs';
 import path from 'path';
 import os from 'os';
@@ -14,6 +13,7 @@ import { DB } from '@matrixai/db';
 import { PromiseCancellable } from '@matrixai/async-cancellable';
 import { EventAll } from '@matrixai/events';
 import { AsyncIterableX as AsyncIterable } from 'ix/asynciterable';
+import { Token } from '@/tokens';
 import TaskManager from '@/tasks/TaskManager';
 import PolykeyAgent from '@/PolykeyAgent';
 import Discovery from '@/discovery/Discovery';
@@ -31,9 +31,9 @@ import * as nodesUtils from '@/nodes/utils';
 import * as discoveryErrors from '@/discovery/errors';
 import * as keysUtils from '@/keys/utils';
 import * as gestaltsUtils from '@/gestalts/utils';
+import { encodeProviderIdentityId } from '@/ids';
 import * as testNodesUtils from '../nodes/utils';
 import TestProvider from '../identities/TestProvider';
-import { encodeProviderIdentityId } from '../../src/ids';
 import 'ix/add/asynciterable-operators/toarray';
 import { createTLSConfig } from '../utils/tls';
 
@@ -77,6 +77,12 @@ describe('Discovery', () => {
     'refreshBucket',
   );
 
+  async function waitForAllDiscoveryTasks(discovery: Discovery) {
+    do {
+      /* Do nothing */
+    } while ((await discovery.waitForDiscoveryTasks()) > 0);
+  }
+
   beforeEach(async () => {
     testProvider = new TestProvider();
     mockedRefreshBucket.mockImplementation(
@@ -290,10 +296,7 @@ describe('Discovery', () => {
     });
     await taskManager.startProcessing();
     await discovery.queueDiscoveryByNode(nodeA.keyRing.getNodeId());
-    let existingTasks: number = 0;
-    do {
-      existingTasks = await discovery.waitForDiscoveryTasks();
-    } while (existingTasks > 0);
+    await waitForAllDiscoveryTasks(discovery);
     const gestalts = await AsyncIterable.as(
       gestaltGraph.getGestalts(),
     ).toArray();
@@ -328,10 +331,7 @@ describe('Discovery', () => {
     });
     await taskManager.startProcessing();
     await discovery.queueDiscoveryByIdentity(testToken.providerId, identityId);
-    let existingTasks: number = 0;
-    do {
-      existingTasks = await discovery.waitForDiscoveryTasks();
-    } while (existingTasks > 0);
+    await waitForAllDiscoveryTasks(discovery);
     const gestalts = await AsyncIterable.as(
       gestaltGraph.getGestalts(),
     ).toArray();
@@ -366,10 +366,7 @@ describe('Discovery', () => {
     });
     await taskManager.startProcessing();
     await discovery.queueDiscoveryByNode(nodeA.keyRing.getNodeId());
-    let existingTasks: number = 0;
-    do {
-      existingTasks = await discovery.waitForDiscoveryTasks();
-    } while (existingTasks > 0);
+    await waitForAllDiscoveryTasks(discovery);
     const gestalts1 = await AsyncIterable.as(
       gestaltGraph.getGestalts(),
     ).toArray();
@@ -399,17 +396,30 @@ describe('Discovery', () => {
       iss: nodesUtils.encodeNodeId(nodeA.keyRing.getNodeId()),
       sub: encodeProviderIdentityId([testProvider.id, identityId2]),
     };
-    const [, signedClaim] = await nodeA.sigchain.addClaim(identityClaim);
-    await testProvider.publishClaim(
-      identityId2,
-      signedClaim as SignedClaim<ClaimLinkIdentity>,
+    await nodeA.sigchain.addClaim(
+      identityClaim,
+      undefined,
+      async (token: Token<ClaimLinkIdentity>) => {
+        // Publishing in the callback to avoid adding bad claims
+        const claim = token.toSigned();
+        const identitySignedClaim = await testProvider.publishClaim(
+          identityId2,
+          claim,
+        );
+        // Append the ProviderIdentityClaimId to the token
+        const payload: ClaimLinkIdentity = {
+          ...claim.payload,
+          providerIdentityClaimId: identitySignedClaim.id,
+        };
+        const newToken = Token.fromPayload(payload);
+        newToken.signWithPrivateKey(nodeA.keyRing.keyPair);
+        return newToken;
+      },
     );
     // Note that eventually we would like to add in a system of revisiting
     // already discovered vertices, however for now we must do this manually.
     await discovery.queueDiscoveryByNode(nodeA.keyRing.getNodeId());
-    do {
-      existingTasks = await discovery.waitForDiscoveryTasks();
-    } while (existingTasks > 0);
+    await waitForAllDiscoveryTasks(discovery);
     const gestalts2 = await AsyncIterable.as(
       gestaltGraph.getGestalts(),
     ).toArray();
@@ -436,7 +446,7 @@ describe('Discovery', () => {
     await discovery.stop();
     await discovery.destroy();
   });
-  test('discovery persistence across restarts', async () => {
+  test('node discovery persistence across restarts', async () => {
     const discovery = await Discovery.createDiscovery({
       db,
       keyRing,
@@ -450,10 +460,7 @@ describe('Discovery', () => {
     await discovery.stop();
     await discovery.start();
     await taskManager.startProcessing();
-    let existingTasks: number = 0;
-    do {
-      existingTasks = await discovery.waitForDiscoveryTasks();
-    } while (existingTasks > 0);
+    await waitForAllDiscoveryTasks(discovery);
     const gestalts = await AsyncIterable.as(
       gestaltGraph.getGestalts(),
     ).toArray();
@@ -476,6 +483,93 @@ describe('Discovery', () => {
     await discovery.stop();
     await discovery.destroy();
   });
+  test('identity discovery persistence across restarts', async () => {
+    const discovery = await Discovery.createDiscovery({
+      db,
+      keyRing,
+      gestaltGraph,
+      identitiesManager,
+      nodeManager,
+      taskManager,
+      logger,
+    });
+    await taskManager.startProcessing();
+    const identityId2 = 'other-gestalt2' as IdentityId;
+    await nodeA.identitiesManager.putToken(testToken.providerId, identityId2, {
+      accessToken: 'ghi789',
+    });
+    testProvider.users[identityId2] = {};
+    for (let i = 0; i < testProvider.pageSize * 2; i++) {
+      const identityClaim = {
+        typ: 'ClaimLinkIdentity',
+        iss: nodesUtils.encodeNodeId(nodeA.keyRing.getNodeId()),
+        sub: encodeProviderIdentityId([testProvider.id, identityId2]),
+      };
+      await nodeA.sigchain.addClaim(
+        identityClaim,
+        undefined,
+        async (token: Token<ClaimLinkIdentity>) => {
+          // Publishing in the callback to avoid adding bad claims
+          const claim = token.toSigned();
+          const identitySignedClaim = await testProvider.publishClaim(
+            identityId2,
+            claim,
+          );
+          // Append the ProviderIdentityClaimId to the token
+          const payload: ClaimLinkIdentity = {
+            ...claim.payload,
+            providerIdentityClaimId: identitySignedClaim.id,
+          };
+          const newToken = Token.fromPayload(payload);
+          newToken.signWithPrivateKey(nodeA.keyRing.keyPair);
+          return newToken;
+        },
+      );
+    }
+
+    // Spy on getClaimsPage
+    let getClaimsPageMockCalls = 0;
+    const firstPageCompletedP = utils.promise();
+    const getClaimsPageMock = jest.spyOn(testProvider, 'getClaimsPage');
+    getClaimsPageMock.mockImplementation(async function* (
+      ...args: Parameters<typeof testProvider.getClaimsPage>
+    ) {
+      const result: ReturnType<typeof testProvider.getClaimsPage> =
+        TestProvider.prototype.getClaimsPage.call(testProvider, ...args);
+      for await (const claim of result) {
+        if (args[1] === identityId2) {
+          if (getClaimsPageMockCalls === testProvider.pageSize) {
+            // Trigger manual task stopping
+            firstPageCompletedP.resolveP();
+          }
+          getClaimsPageMockCalls++;
+        }
+        yield claim;
+      }
+    });
+    await discovery.queueDiscoveryByIdentity(testToken.providerId, identityId2);
+
+    await firstPageCompletedP.p;
+    await taskManager.stopProcessing();
+    await discovery.stop();
+    await discovery.start();
+    await taskManager.startProcessing();
+
+    await waitForAllDiscoveryTasks(discovery);
+    // This total claims gotten should be above 2 pages worth of claims, because there will be some overlap
+    expect(getClaimsPageMockCalls).toBeGreaterThanOrEqual(
+      2 * testProvider.pageSize,
+    );
+    // This total claims gotten should be below 3 pages worth of claims,
+    // because the overlap of claims should never exceed the no. of pages - 1
+    expect(getClaimsPageMockCalls).toBeLessThan(3 * testProvider.pageSize);
+
+    delete testProvider.users[identityId2];
+    getClaimsPageMock.mockReset();
+    await taskManager.stopProcessing();
+    await discovery.stop();
+    await discovery.destroy();
+  });
   test('processed vertices are queued for rediscovery', async () => {
     const discovery = await Discovery.createDiscovery({
       db,
@@ -489,10 +583,8 @@ describe('Discovery', () => {
     });
     await taskManager.startProcessing();
     await discovery.queueDiscoveryByNode(nodeA.keyRing.getNodeId());
-    let existingTasks: number = 0;
-    do {
-      existingTasks = await discovery.waitForDiscoveryTasks();
-    } while (existingTasks > 0);
+
+    await waitForAllDiscoveryTasks(discovery);
     await discovery.waitForDiscoveryTasks(true);
 
     await taskManager.stopProcessing();
@@ -516,18 +608,14 @@ describe('Discovery', () => {
 
     // Attempt initial discovery
     await discovery.queueDiscoveryByNode(nodeA.keyRing.getNodeId());
-    let existingTasks: number = 0;
-    do {
-      existingTasks = await discovery.waitForDiscoveryTasks();
-    } while (existingTasks > 0);
+
+    await waitForAllDiscoveryTasks(discovery);
     // All the vertices should be processed
     expect(processVertexMock).toHaveBeenCalledTimes(3);
     // 2nd attempt at discovery
     processVertexMock.mockReset();
     await discovery.queueDiscoveryByNode(nodeA.keyRing.getNodeId());
-    do {
-      existingTasks = await discovery.waitForDiscoveryTasks();
-    } while (existingTasks > 0);
+    await waitForAllDiscoveryTasks(discovery);
     // Only the queued vertex should be processed
     expect(processVertexMock).toHaveBeenCalledTimes(1);
 
@@ -552,18 +640,14 @@ describe('Discovery', () => {
 
     // Attempt initial discovery
     await discovery.queueDiscoveryByNode(nodeA.keyRing.getNodeId());
-    let existingTasks: number = 0;
-    do {
-      existingTasks = await discovery.waitForDiscoveryTasks();
-    } while (existingTasks > 0);
+
+    await waitForAllDiscoveryTasks(discovery);
     // All the vertices should be processed
     expect(processVertexMock).toHaveBeenCalledTimes(3);
     // 2nd attempt at discovery
     processVertexMock.mockClear();
     await discovery.queueDiscoveryByNode(nodeA.keyRing.getNodeId(), Date.now());
-    do {
-      existingTasks = await discovery.waitForDiscoveryTasks();
-    } while (existingTasks > 0);
+    await waitForAllDiscoveryTasks(discovery);
     // All vertices should be reprocessed
     expect(processVertexMock).toHaveBeenCalledTimes(3);
 
@@ -590,10 +674,7 @@ describe('Discovery', () => {
     });
     await taskManager.startProcessing();
     await discovery.queueDiscoveryByNode(nodeA.keyRing.getNodeId());
-    let existingTasks: number = 0;
-    do {
-      existingTasks = await discovery.waitForDiscoveryTasks();
-    } while (existingTasks > 0);
+    await waitForAllDiscoveryTasks(discovery);
 
     // Just checking basic functionality of queued and processed
     expect(eventMap.get(discoveryEvents.EventDiscoveryVertexQueued.name)).toBe(
diff --git a/tests/identities/TestProvider.ts b/tests/identities/TestProvider.ts
index 1f4da88fa..b451c6342 100644
--- a/tests/identities/TestProvider.ts
+++ b/tests/identities/TestProvider.ts
@@ -5,6 +5,7 @@ import type {
   ProviderToken,
   IdentityData,
   ProviderAuthenticateRequest,
+  ProviderPaginationToken,
 } from '@/identities/types';
 import type {
   IdentitySignedClaim,
@@ -19,6 +20,7 @@ import * as tokenUtils from '@/tokens/utils';
 
 class TestProvider extends Provider {
   public readonly id: ProviderId;
+  public readonly pageSize = 10;
 
   public linkIdCounter: number = 0;
   public users: Record<IdentityId, POJO>;
@@ -171,6 +173,43 @@ class TestProvider extends Provider {
     };
   }
 
+  public async *getClaimIdsPage(
+    authIdentityId: IdentityId,
+    identityId: IdentityId,
+    paginationToken?: ProviderPaginationToken | undefined,
+  ): AsyncGenerator<{
+    claimId: ProviderIdentityClaimId;
+    nextPaginationToken?: ProviderPaginationToken;
+  }> {
+    const providerToken = await this.getToken(authIdentityId);
+    let startIndex = paginationToken == null ? 0 : parseInt(paginationToken);
+    if (isNaN(startIndex)) {
+      startIndex = 0;
+    }
+    if (!providerToken) {
+      throw new identitiesErrors.ErrorProviderUnauthenticated(
+        `${authIdentityId} has not been authenticated`,
+      );
+    }
+    await this.checkToken(providerToken, authIdentityId);
+    const claimIds =
+      this.userLinks[identityId].slice(
+        startIndex,
+        this.pageSize + startIndex,
+      ) ?? [];
+    for (const [i, claimId] of claimIds.entries()) {
+      yield {
+        claimId,
+        nextPaginationToken:
+          i === claimIds.length - 1
+            ? ((
+                startIndex + this.pageSize
+              ).toString() as ProviderPaginationToken)
+            : undefined,
+      };
+    }
+  }
+
   public async getClaim(
     authIdentityId: IdentityId,
     claimId: ProviderIdentityClaimId,
@@ -196,26 +235,6 @@ class TestProvider extends Provider {
       url: 'test.com',
     };
   }
-
-  public async *getClaims(
-    authIdentityId: IdentityId,
-    identityId: IdentityId,
-  ): AsyncGenerator<IdentitySignedClaim> {
-    const providerToken = await this.getToken(authIdentityId);
-    if (!providerToken) {
-      throw new identitiesErrors.ErrorProviderUnauthenticated(
-        `${authIdentityId} has not been authenticated`,
-      );
-    }
-    await this.checkToken(providerToken, authIdentityId);
-    const claimIds = this.userLinks[identityId] ?? [];
-    for (const claimId of claimIds) {
-      const claimInfo = await this.getClaim(authIdentityId, claimId);
-      if (claimInfo != null) {
-        yield claimInfo;
-      }
-    }
-  }
 }
 
 export default TestProvider;
