Skip to Content
Core ConceptsMemory Sharing

Memory Sharing

zkStash supports permissionless memory sharing between agents using cryptographic grants. This enables collaborative AI systems where agents can selectively share their memories without requiring central administration.

How It Works

Memory sharing in zkStash is built on three principles:

  1. Cryptographic Authorization: Grants are signed messages that prove the grantor’s consent
  2. Permissionless Verification: Any agent can verify a grant without contacting the grantor
  3. Time-Bounded Access: Grants automatically expire—no revocation needed

Grant Structure

A grant is a signed JSON payload:

interface GrantPayload { f: string; // "from" - grantor wallet address (the signer) g: string; // "grantee" - wallet address receiving access a?: string; // Optional: limit to specific agentId u?: string; // Optional: limit to specific subjectId (tenant) e: number; // Expiration timestamp (unix seconds) } interface SignedGrant { p: GrantPayload; // The signed payload s: string; // Signature (hex for EVM, base64 for Solana) c: "evm" | "sol"; // Chain type for verification }

Fields explained:

  • f (from): The wallet address of the agent sharing their memories
  • g (grantee): The wallet address of the agent receiving access
  • a (agentId): Optional scope—if set, access is limited to memories from this agent
  • u (subjectId): Optional scope—if set, access is limited to memories from this subject/tenant
  • e (expiry): Unix timestamp when the grant expires

Creating Grants

Grants are created client-side using your wallet to sign the payload. No server registration is required.

import { fromPrivateKey } from "@zkstash/sdk/rest"; // Agent A creates a grant for Agent B const agentA = await fromPrivateKey(process.env.AGENT_A_KEY!); const { grant, shareCode } = await agentA.createGrant({ grantee: "0xAgentBAddress...", // B's wallet agentId: "researcher", // Optional: limit scope duration: "7d", // Valid for 7 days }); // Share the code via any channel (API, message, etc.) console.log("Share code:", shareCode); // Output: "zkg1_eyJwIjp7ImYiOiIweC4uLi..."

Duration Format

The duration parameter accepts human-readable strings:

  • "1h" - 1 hour
  • "24h" - 24 hours
  • "7d" - 7 days
  • "30d" - 30 days

Using Grants

Agent B can use the grant to search Agent A’s memories:

import { fromPrivateKey } from "@zkstash/sdk/rest"; const agentB = await fromPrivateKey(process.env.AGENT_B_KEY!); // Add the grant (accepts share code string or SignedGrant object) agentB.addGrant(shareCode); // Now searches include Agent A's shared memories const results = await agentB.searchMemories({ query: "research findings", filters: { agentId: "researcher" }, }); // Results are annotated with their source results.memories.forEach((m) => { if (m.source === "shared") { console.log(`From ${m.grantor}:`, m.data); } else { console.log("Own:", m.data); } });

Search Scopes

Control which memories to include using the scope parameter:

ScopeSearches OwnSearches SharedUse Case
"own"Only your memories, ignore grants
"shared"Only granted memories (requires grants)
"all"Both (default behavior)
// Search only your own memories await client.searchMemories( { query: "preferences", filters: { agentId: "my-agent" } }, { scope: "own" } ); // Search only shared memories await client.searchMemories( { query: "findings", filters: { agentId: "researcher" } }, { scope: "shared" } ); // Search everything (default) await client.searchMemories( { query: "all context", filters: { agentId: "any" } } // scope defaults to "all" );

Grant Management

Instance-Level Grants

Add grants to your client instance for automatic inclusion in all searches:

// Add grants (accepts SignedGrant or share code) client.addGrant(shareCode); client.addGrant(grantObject); // Remove a grant client.removeGrant(grantObject); // List all instance grants const grants = client.getInstanceGrants();

Per-Request Grants

Pass grants for a single request without storing them:

await client.searchMemories( { query: "...", filters: { agentId: "..." } }, { grants: [oneTimeGrant] } );

Grantee Identity

The g (grantee) field identifies who can use the grant. This depends on how the grantee authenticates:

Grantee AuthWhat to use as gExample
WalletWallet address0x1234...abcd
API KeyClerk userId (API key owner)user_2abc123xyz

For Wallet Users

Use the grantee’s wallet address:

await grantor.createGrant({ grantee: "0xAgentBWalletAddress...", duration: "7d", });

For API Key Users

Use the Clerk userId of the API key owner. This is displayed in the zkStash dashboard:

await grantor.createGrant({ grantee: "user_2abc123xyz...", // Clerk userId duration: "7d", });

When verifying, zkStash checks if g matches either the caller’s:

  • userId (wallet address for wallet auth, keyId for API key auth)
  • payerId (Clerk userId for API key auth)

This means a grant targeting a Clerk userId works for all API keys owned by that user.

Security Considerations

Grantee-Bound Access

Unlike classic bearer tokens, grants cannot be used by anyone. Each grant is cryptographically bound to a specific grantee:

Attacker intercepts grant → Tries to use it → API checks: caller ≠ grantee → Access denied ✅

Only the designated grantee (matching g field) can use the grant. However, you should still:

  • Share codes over secure channels
  • Use short expiration times when possible
  • Avoid logging grants in plain text

No Revocation

Grants cannot be revoked—they expire automatically. If a grantee’s credentials are compromised:

  1. Wait for grant expiration
  2. Don’t issue new grants to that grantee

For sensitive use cases, use shorter durations (hours instead of days).

Verification Flow

zkStash verifies every grant before use:

  1. Signature validity: The grantor actually signed this grant
  2. Expiration: The grant hasn’t expired
  3. Grantee match: The caller’s identity matches g (wallet address OR Clerk userId)
  4. Agent scope: If a is set, access is limited to that agentId

If any check fails, the grant is silently ignored.

Use Cases

Research Collaboration

Multiple research agents share findings with a coordinator:

// Researcher agents grant access to coordinator const grant = await researcher.createGrant({ grantee: coordinatorAddress, agentId: "research-findings", duration: "30d", });

Customer Support Handoff

Support agents share customer context during escalation:

const grant = await tier1Agent.createGrant({ grantee: tier2AgentAddress, duration: "4h", // Short duration for handoff });

Multi-Agent Systems

Agents in a workflow share specialized knowledge:

// Planner shares context with executor await planner.createGrant({ grantee: executorAddress, agentId: "task-plans", duration: "24h", });

REST API

Grants are passed via the X-Grants header (base64-encoded JSON array):

# Encode grants GRANTS=$(echo '[{"p":{"f":"0x...","g":"0x...","e":1735600000},"s":"0x...","c":"evm"}]' | base64) # Search with grants curl "https://api.zkstash.ai/v1/memories/search?query=test&agentId=demo&scope=all" \ -H "Authorization: Bearer $TOKEN" \ -H "X-Grants: $GRANTS"

Response Format

Search results include source annotations:

{ "success": true, "memories": [ { "id": "mem_123", "kind": "UserProfile", "data": { "name": "Alice" }, "score": 0.95, "source": "own" }, { "id": "mem_456", "kind": "ResearchFinding", "data": { "topic": "AI agents" }, "score": 0.89, "source": "shared", "grantor": "0xAgentAAddress..." } ] }

Limits

  • Maximum 10 grants per search request
  • Grant duration is validated (must be in the future)
  • Invalid/expired grants are silently skipped
Last updated on