SAP DOCv0.9.3
SDK Reference

Metaplex Bridge

Link a SAP agent to a Metaplex Core asset through the AgentIdentity external plugin. Covers triple-check audits, atomic register flows, EIP-8004 hosting, and authenticated RPC reads.

Metaplex Bridge

client.metaplex connects a SAP agent to a Metaplex Core asset using the AgentIdentity external plugin. The plugin is asset-only and stores one field: a URI pointing to an EIP-8004 registration JSON. Capabilities, services, executives, and reputation live in that JSON, served live from the SAP indexer.

FieldValue
Moduleclient.metaplex
SinceSDK 0.9.0 (atomic flows: 0.9.3)
Peer@metaplex-foundation/mpl-core >= 1.9.0
                ┌──────────────────────────────────────┐
                │   MPL Core Asset (transferable NFT)  │
                │   AgentIdentity.uri ──┐              │
                └───────────────────────┼──────────────┘

   https://explorer.oobeprotocol.ai/agents/<sapAgentPda>/eip-8004.json


        SAP indexer ◀── reads ── AgentAccount + VaultDelegate*

One MPL transaction attaches the plugin. After that, every SAP write propagates with zero MPL transactions because the JSON is rendered live from on-chain SAP state.

Decision matrix

You want toUse
Mint a tradeable NFT identity for an existing SAP agentbuildAttachAgentIdentityIx(...)
Migrate an asset to a new registry hostbuildUpdateAgentIdentityUriIx(...)
Render an explorer page for one agent (image + on-chain stats)getUnifiedProfile({ asset, rpcUrl, rpcHeaders? })
Confirm an asset cryptographically links to a SAP PDAverifyLink({ asset, sapAgentPda, rpcUrl, rpcHeaders? })
Three-layer link audit (mpl-core + EIP-8004 JSON + SAP PDA)tripleCheckLink({ asset, expectedOwner?, rpcUrl, rpcHeaders? })
Mint MPL Core asset and attach AgentIdentity in one tx (SAP exists)buildMintAndAttachIxs(opts)
Register SAP for an existing MPL asset's owner (idempotent)buildRegisterSapForMplOwnerIx(opts)
Atomic both-sided register (start fresh)buildRegisterBothIxs(opts)
Serve the EIP-8004 JSON from your own hostbuildEip8004Registration({ sapAgentOwner, services })
Compute the canonical URL without fetchingderiveRegistrationUrl(sapAgentPda, baseUrl)

Linking flow (single transaction)

import { SapClient } from "@oobe-protocol-labs/synapse-sap-sdk";
import { Transaction } from "@solana/web3.js";

const client = SapClient.from(provider);

const ix = await client.metaplex.buildAttachAgentIdentityIx({
  asset:               mplCoreAsset,
  authority:           wallet.publicKey,
  sapAgentOwner:       wallet.publicKey,
  registrationBaseUrl: "https://explorer.oobeprotocol.ai",
  rpcUrl:              process.env.RPC_URL!,
});

await provider.sendAndConfirm(new Transaction().add(ix));

What happens on-chain:

  1. mpl_core adds an AgentIdentity adapter to the asset with uri = https://explorer.oobeprotocol.ai/agents/<sapAgentPda>/eip-8004.json and lifecycle check [Execute, CanApprove].
  2. SAP state is untouched: no SAP transaction, no SAP fee.

What happens off-chain afterwards:

  • Whenever the SAP agent's capabilities, vault delegates, or x402 tiers change, the served JSON updates automatically.
  • The MPL plugin never needs to be touched again unless you migrate hosts (use buildUpdateAgentIdentityUriIx).

Reading a unified profile

const profile = await client.metaplex.getUnifiedProfile({
  asset:  mplCoreAsset,
  rpcUrl: process.env.RPC_URL!,
});

if (profile.linked) {
  console.log("Agent name :", profile.mpl?.registration?.name);
  console.log("Capabilities:", profile.sap.identity?.capabilities);
  console.log("Reputation :", profile.sap.stats?.reputationScore);
}

profile.linked is true when:

  1. AgentIdentity.uri ends with /agents/<sapAgentPda>/eip-8004.json, and
  2. The fetched JSON's synapseAgent field equals the SAP PDA.

This is what makes the link bidirectional and cryptographic without any on-chain SAP change.

tripleCheckLink runs three independent checks and reports each layer:

const result = await client.metaplex.tripleCheckLink({
  asset:         assetPk,
  expectedOwner: walletPk,
  rpcUrl,
  rpcHeaders,   // required on gated RPCs
});

result.layers; // { mplOnChain, eip8004Json, sapOnChain }
result.linked; // true ⇔ all three layers pass
LayerPasses whenCommon failure cause
mplOnChainAsset readable + AgentIdentity plugin presentRPC blocks fetchAsset (401), asset not found, no plugin attached
eip8004JsonagentIdentityUri resolves AND JSON synapseAgent === sapPdaURI points to a foreign registry (e.g. api.metaplex.com/v1/agents/...)
sapOnChainAgentAccount PDA exists for the asset's ownerWallet never registered a SAP agent

Drive next-step actions from partial passes:

StateNext action
mpl + sap, no eipUpdate the plugin URI to point to the SAP host (buildUpdateAgentIdentityUriIx)
sap onlyMint identity NFT and attach (buildMintAndAttachIxs)
mpl onlyRegister the asset owner on SAP (buildRegisterSapForMplOwnerIx)
noneAtomic both-sided register (buildRegisterBothIxs)

Authenticated RPC reads

Every read method accepts an optional rpcHeaders?: Record<string, string> that the bridge injects into umi's HTTP client. This is required when your RPC enforces auth (Synapse Gateway, Triton, QuickNode, etc.). Without it fetchAsset silently returns 401 and the read layers report false.

import { getRpcConfig } from "@/lib/sap/discovery";

const { url, headers } = getRpcConfig();
const result = await client.metaplex.tripleCheckLink({
  asset:         new PublicKey(assetId),
  expectedOwner: new PublicKey(walletId),
  rpcUrl:        url,
  rpcHeaders:    headers,
});

Three register flows

Flow A: SAP exists, mint MPL and attach (2 ixs in 1 tx)

const ixs = await client.metaplex.buildMintAndAttachIxs({
  sapAgentOwner: wallet,
  authority:     wallet,
  payer:         wallet,
  owner:         wallet,
  name:          "My Agent",
  metadataUri:   "https://...metadata.json",
  registrationBaseUrl: "https://explorer.oobeprotocol.ai",
  rpcUrl:        process.env.RPC_URL!,
});

await provider.sendAndConfirm(new Transaction().add(...ixs));

Flow B: MPL asset exists, register SAP for its owner (idempotent)

const ix = await client.metaplex.buildRegisterSapForMplOwnerIx({
  asset:        mplCoreAsset,
  rpcUrl:       process.env.RPC_URL!,
  rpcHeaders,
  registration: { name: "My Agent", description: "...", capabilities: [...] },
});

if (ix) await provider.sendAndConfirm(new Transaction().add(ix));

ix is null when the SAP agent already exists. Safe to call repeatedly.

Flow C: atomic both-sided (no SAP, no MPL)

const ixs = await client.metaplex.buildRegisterBothIxs({
  payer: wallet,
  owner: wallet,
  authority: wallet,
  sapRegistration:  { name: "My Agent", capabilities: [...] },
  mplAsset:         { name: "My Agent NFT", metadataUri: "https://..." },
  registrationBaseUrl: "https://explorer.oobeprotocol.ai",
  rpcUrl: process.env.RPC_URL!,
});

await provider.sendAndConfirm(new Transaction().add(...ixs));

A single transaction registers SAP, mints the MPL Core asset, and attaches the AgentIdentity plugin. Use this for new agents that need both sides from the start.

Hosting the EIP-8004 endpoint

Any registry host can serve the canonical URL. Example with Next.js:

// app/api/agents/[wallet]/eip-8004.json/route.ts
import { SapClient } from "@oobe-protocol-labs/synapse-sap-sdk";
import { PublicKey } from "@solana/web3.js";

export async function GET(
  _req: Request,
  { params }: { params: { wallet: string } },
) {
  const client = SapClient.from(getServerProvider());
  const json = await client.metaplex.buildEip8004Registration({
    sapAgentOwner: new PublicKey(params.wallet),
    services: [
      { id: "x402-default", type: "x402-endpoint", url: `https://...` },
    ],
  });
  return Response.json(json, {
    headers: { "Cache-Control": "public, max-age=15, s-maxage=60" },
  });
}

The synapse-sap-explorer ships this endpoint at /agents/[wallet]/eip-8004.json, ready to use as the canonical host.

Efficiency comparison

OperationNaive design (dual on-chain)This bridge
Initial linking2 tx (SAP + MPL addExecutive)1 tx (MPL only)
Add a vault delegate2 tx1 tx (SAP only)
Revoke a delegate2 tx1 tx (SAP only)
Capability add / x402 tier change2 tx1 tx (SAP only)
Reads per profile2 RPC + 2 deserializations1 RPC + 1 fetch (cached)
MPL programs touched after initevery changenever (until host migration)
Required on-chain SAP changesnew instructions + new fieldszero

Common pitfalls

PitfallReality
"Use addExecutive to delegate"Function does not exist in mpl-core >= 1.9.0
"AgentIdentity stores the executive list"It stores only uri: string
"We need a new SAP instruction to link"The link is just the URI in the MPL plugin. SAP is unchanged
"Attach the plugin to the collection"AgentIdentity is rejected on collections by validate_create
"Two plugins per asset"One per asset, enforced by add_external_plugin_adapter
"umi Signer requires a real keypair"For ix-only construction the bridge uses a public-key stub. The caller signs the assembled web3.js transaction

Quick reference

client.metaplex.deriveRegistrationUrl(sapPda, baseUrl);
client.metaplex.buildEip8004Registration({ sapAgentOwner, services });
client.metaplex.buildAttachAgentIdentityIx(opts);
client.metaplex.buildUpdateAgentIdentityUriIx(opts);
client.metaplex.getUnifiedProfile({ asset|wallet, rpcUrl, rpcHeaders? });
client.metaplex.verifyLink({ asset, sapAgentPda, rpcUrl, rpcHeaders? });
client.metaplex.tripleCheckLink({ asset, expectedOwner?, rpcUrl, rpcHeaders? });
client.metaplex.buildMintAndAttachIxs(opts);
client.metaplex.buildRegisterSapForMplOwnerIx(opts);
client.metaplex.buildRegisterBothIxs(opts);

See also