Off-chain attestations
The same schemas, the same fields, but the attestation is signed and stored locally instead of broadcast to chain. Zero gas. Verifiable cryptographically by anyone with the envelope and the signer’s address.
When to use off-chain
Section titled “When to use off-chain”- Free-tier flows where users don’t have crypto and you don’t want to bear gas.
- High-volume pipelines where on-chain cost is prohibitive.
- Pre-publication staging: sign now, decide later whether to bring it on-chain.
- Privacy-sensitive content where even publishing the hash is undesirable until needed.
Complete, copy-pasteable runnable. Off-chain signing needs a private key but no RPC connectivity. See Configuration for every constructor option.
import { Fidemark, getNetwork } from "@fidemark/sdk";
const fidemark = new Fidemark({ network: getNetwork("base-sepolia"), privateKey: process.env.PRIVATE_KEY,});
const envelope = await fidemark.attestHumanOffchain({ content: myArticle, contentType: "text/article",});
const aiEnvelope = await fidemark.attestAIOffchain({ content: aiOutput, modelId: "claude-sonnet-4-6", provider: "anthropic", prompt: userPrompt,});import osfrom fidemark import AttestAIInput, AttestHumanInput, Fidemark, get_network
fidemark = Fidemark( network=get_network("base-sepolia"), private_key=os.environ["PRIVATE_KEY"],)
envelope = fidemark.attest_human_offchain(AttestHumanInput( content=my_article, content_type="text/article",))
ai_envelope = fidemark.attest_ai_offchain(AttestAIInput( content=ai_output, model_id="claude-sonnet-4-6", provider="anthropic", prompt=user_prompt,))import ( "context" "log" "os"
"github.com/fidemark/sdk-go/fidemark")
network, _ := fidemark.GetNetwork("base-sepolia")client, err := fidemark.New(fidemark.Config{ Network: network, PrivateKey: os.Getenv("PRIVATE_KEY"),})if err != nil { log.Fatal(err) }
envelope, err := client.AttestHumanOffchain(context.Background(), fidemark.AttestHumanInput{ Content: []byte(myArticle), ContentType: "text/article",}, fidemark.SignOptions{})
aiEnvelope, err := client.AttestAIOffchain(context.Background(), fidemark.AttestAIInput{ Content: []byte(aiOutput), ModelID: "claude-sonnet-4-6", Provider: "anthropic", Prompt: []byte(userPrompt),}, fidemark.SignOptions{})All three return an OffchainEnvelope with: type, uid (EAS-derived, deterministic from the signed payload), attester, network, signed (the full EAS V2 payload + signature), and decoded convenience fields. The envelope is JSON-serializable; share it over any transport (HTTP, IPFS, Slack, email).
Verify
Section titled “Verify”const valid = fidemark.verifyOffchain(envelope);// true iff signature recovers to envelope.attester AND payload + network matchvalid = fidemark.verify_offchain(envelope)valid, err := client.VerifyOffchain(envelope)Pure cryptography, no RPC. The check fails when:
- The signature doesn’t recover to
envelope.attester(tampered signed payload, wrong attester). - The envelope was signed for a different chain or EAS contract.
- The schema in the signed payload doesn’t match this network’s expected schema for
envelope.type.
Bring on-chain later
Section titled “Bring on-chain later”A signed envelope can be promoted to an on-chain attestation at any time, and someone other than the original attester can pay gas to do it. The mechanism is EAS’s delegated-attestation flow, available in all three SDKs.
Step 1: sign with delegation enabled
Section titled “Step 1: sign with delegation enabled”Alice (the original attester) signs an off-chain envelope and asks the SDK to also produce the matching delegated EIP-712 signature.
import { Fidemark, getNetwork } from "@fidemark/sdk";
const alice = new Fidemark({ network: getNetwork("base-sepolia"), privateKey: process.env.ALICE_KEY,});
const envelope = await alice.attestHumanOffchain( { content: myArticle, contentType: "text/article" }, { signWithDelegated: true },);// envelope.delegated is now populated: a second EIP-712 signature, plus// the attester's nonce and an optional deadline.import osfrom fidemark import AttestHumanInput, Fidemark, get_network
alice = Fidemark( network=get_network("base-sepolia"), private_key=os.environ["ALICE_KEY"],)
envelope = alice.attest_human_offchain( AttestHumanInput(content=my_article, content_type="text/article"), sign_with_delegated=True,)# envelope.delegated is populated with the EAS-delegated signature,# the attester's nonce, and an optional deadline.import ( "context" "log" "os"
"github.com/fidemark/sdk-go/fidemark")
network, _ := fidemark.GetNetwork("base-sepolia")alice, err := fidemark.New(fidemark.Config{ Network: network, PrivateKey: os.Getenv("ALICE_KEY"),})if err != nil { log.Fatal(err) }
envelope, err := alice.AttestHumanOffchain(context.Background(), fidemark.AttestHumanInput{ Content: []byte(myArticle), ContentType: "text/article",}, fidemark.SignOptions{SignWithDelegated: true})// envelope.Delegated carries the EAS-delegated signature, nonce, and deadline.Step 2: anyone publishes
Section titled “Step 2: anyone publishes”Bob (any wallet with funds) takes Alice’s signed envelope and publishes it on-chain. The on-chain record names Alice as the attester; Bob just paid gas.
import { Fidemark, getNetwork } from "@fidemark/sdk";
const bob = new Fidemark({ network: getNetwork("base-sepolia"), privateKey: process.env.BOB_KEY,});
const result = await bob.publishOffchain(envelope);// result.uid : NEW on-chain UID (different from envelope.uid)// result.txHash : bob's transaction hash// result.verifyUrl: public verify URLimport osfrom fidemark import Fidemark, get_network
bob = Fidemark( network=get_network("base-sepolia"), private_key=os.environ["BOB_KEY"],)
result = bob.publish_offchain(envelope)# result.uid : new on-chain UID (different from envelope.uid)# result.tx_hash : bob's transaction hash# result.verify_url : public verify URLimport ( "context" "log" "os"
"github.com/fidemark/sdk-go/fidemark")
network, _ := fidemark.GetNetwork("base-sepolia")bob, err := fidemark.New(fidemark.Config{ Network: network, PrivateKey: os.Getenv("BOB_KEY"),})if err != nil { log.Fatal(err) }
res, err := bob.PublishOffchain(context.Background(), envelope)// res.UID : new on-chain UID (different from envelope.UID)// res.TxHash : bob's transaction hash// res.VerifyURL : public verify URLOn-chain, attestation.attester is recorded as alice, not bob. Bob just paid gas.
Caveats
Section titled “Caveats”- The on-chain UID differs from the off-chain UID. EAS stamps
time = block.timestampwhen the attestation lands, and the UID is hashed from the attestation data including time. The off-chain envelope remains independently verifiable forever. - Each delegated signature is single-use. EAS’s nonce increments after the publish; the same envelope can’t be replayed.
- Mismatched network = rejection.
publishOffchainthrowsINVALID_INPUTbefore any RPC call. - No delegated signature, no promotion. Envelopes signed without
signWithDelegated: truecan still be verified off-chain forever, but a third party can’t bring them on-chain.
Comparison
Section titled “Comparison”| Property | On-chain | Off-chain |
|---|---|---|
| Gas cost (signer) | ~$0.001-0.09 on Base | $0 |
| Storage | EAS contract | wherever you put the envelope |
| Verification | RPC call | pure crypto |
| Revocation | on-chain revoke | not yet, bring on-chain first via publishOffchain |
| Bring on-chain later | n/a | yes, sign with signWithDelegated: true |
| Cross-protocol indexing | EAS GraphQL, Etherscan | none until promoted on-chain |