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:
- Cryptographic Authorization: Grants are signed messages that prove the grantor’s consent
- Permissionless Verification: Any agent can verify a grant without contacting the grantor
- 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 memoriesg(grantee): The wallet address of the agent receiving accessa(agentId): Optional scope—if set, access is limited to memories from this agentu(subjectId): Optional scope—if set, access is limited to memories from this subject/tenante(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:
| Scope | Searches Own | Searches Shared | Use 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 Auth | What to use as g | Example |
|---|---|---|
| Wallet | Wallet address | 0x1234...abcd |
| API Key | Clerk 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:
- Wait for grant expiration
- 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:
- Signature validity: The grantor actually signed this grant
- Expiration: The grant hasn’t expired
- Grantee match: The caller’s identity matches
g(wallet address OR Clerk userId) - Agent scope: If
ais 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