Skip to main content
When a user attaches a file, FileProvenanceTag searches the provenance system in the background and shows one of three states: found (the file is known), not found (the file is new — show ownership claim), or loading. FileOwnershipClaim handles the not-found state: it asks the user whether they own the file, records it on-chain, and returns the CID.

FileProvenanceTag

Shows below an uploaded file attachment. Runs uploadAndMatch on mount, then:
  • Found — shows the creator name (or match %) with an expandable bundle summary
  • Not found + onClaim provided — shows FileOwnershipClaim inline
  • Not found, no onClaim — shows a plain “No prior provenance” label
  • Loading — shows a spinner while searching
import { FileProvenanceTag } from "@provenancekit/ui";

// Minimal — just shows match info, no ownership claim
<FileProvenanceTag
  file={uploadedFile}
  onViewDetail={(cid) => router.push(`/provenance/${cid}`)}
/>

// With ownership claim (shown when file has no prior provenance)
<FileProvenanceTag
  file={uploadedFile}
  onViewDetail={(cid) => router.push(`/provenance/${cid}`)}
  onClaim={async (owned) => {
    const res = await fetch("/api/claim", {
      method: "POST",
      body: buildFormData(uploadedFile, owned, userId),
    });
    const { cid, status } = await res.json();
    return { cid, status }; // "claimed" | "referenced"
  }}
  topK={3}
/>

Props

PropTypeDefaultDescription
fileFile | BlobrequiredThe file to search for
onViewDetail(cid: string) => voidCalled when user clicks “View full →“
onClaim(owned: boolean) => Promise<{ cid: string; status: "claimed" | "referenced" }>Ownership claim callback. If provided, shown when status === "not-found"
topKnumber3Max results to request from the search API
classNamestringAdditional CSS classes

onClaim callback

The onClaim callback receives a boolean:
ownedMeaningSuggested action type
trueUser created the fileaction.type = "create"
falseFile is from an external/unknown sourceaction.type = "reference"
The callback must return { cid: string; status: "claimed" | "referenced" }. The returned CID should be included as inputCids on any downstream provenance action.

FileOwnershipClaim

A standalone component that shows the ownership question, handles loading and success states. FileProvenanceTag renders this automatically in the not-found state when onClaim is provided — but you can also use it directly.
import { FileOwnershipClaim } from "@provenancekit/ui";

<FileOwnershipClaim
  onClaim={async (owned) => {
    const res = await fetch("/api/claim", {
      method: "POST",
      body: JSON.stringify({ fileId, owned, userId }),
      headers: { "Content-Type": "application/json" },
    });
    const { cid } = await res.json();
    return { cid, status: owned ? "claimed" : "referenced" };
  }}
/>
States rendered:
StateUI
idle”New file — do you own this?” + [Yes, I own it] [No, I don’t]
claimingSpinner + “Recording provenance…”
claimed✓ “Claimed as your work” (green)
referenced✓ “Recorded as external source” (blue)
error”Failed to record — retry”

Props

PropTypeDescription
onClaim(owned: boolean) => Promise<FileOwnershipClaimResult>required — called when user makes a decision
classNamestringAdditional CSS classes

Full Integration Example

Here is the full integration pattern used in the PK Chat example app:

1. Upload route stores file to IPFS and returns a CID

// app/api/media/upload/route.ts
import { NextRequest, NextResponse } from "next/server";

export async function POST(req: NextRequest) {
  const form = await req.formData();
  const file = form.get("file") as File;

  // Upload to Pinata/IPFS for a persistent URL
  const pinata = await uploadToPinata(file, process.env.PINATA_JWT!);
  return NextResponse.json({
    url: pinata.url,  // https://gateway.pinata.cloud/ipfs/Qm...
    cid: pinata.cid,  // Qm... — use this as inputCid later
    mimeType: file.type,
    name: file.name,
  });
}

2. Claim route records ownership in ProvenanceKit

// app/api/claim/route.ts
import { getPKClient } from "@/lib/pk-client";

export async function POST(req: NextRequest) {
  const form = await req.formData();
  const file = form.get("file") as File;
  const owned = form.get("owned") === "true";
  const userId = form.get("userId") as string;

  const pk = getPKClient();
  const entityId = await pk.entity({ role: "human", name: userId });

  const result = await pk.file(file, {
    entity: { id: entityId, role: "human", name: userId },
    action: {
      type: owned ? "create" : "reference",
    },
    resourceType: file.type.startsWith("image/") ? "image" : "text",
  });
  // On-chain recording fires automatically if CHAIN_PRIVATE_KEY is set.

  return NextResponse.json({
    cid: result.cid,
    actionId: result.actionId,
    status: owned ? "claimed" : "referenced",
  });
}

3. AttachmentChip wires everything together

function AttachmentChip({ file, userId, onCidAssigned }) {
  async function handleClaim(owned: boolean) {
    const form = new FormData();
    form.append("file", file, file.name);
    form.append("owned", String(owned));
    form.append("userId", userId ?? "anonymous");
    const res = await fetch("/api/claim", { method: "POST", body: form });
    if (!res.ok) throw new Error("Claim failed");
    const data = await res.json();
    onCidAssigned(data.cid); // update CID in parent state
    return { cid: data.cid, status: data.status };
  }

  return (
    <div>
      <span>{file.name}</span>
      <FileProvenanceTag
        file={file}
        onClaim={handleClaim}
        onViewDetail={(cid) => router.push(`/provenance/${cid}`)}
      />
    </div>
  );
}

4. CID flows into the AI response provenance action

// When the message is sent, claimed CIDs are included as inputCids
const response = await pk.file(responseBlob, {
  entity: { id: agentId, role: "ai" },
  action: {
    type: "generate",
    inputCids: [promptCid, ...attachmentCids],  // ← attachment CIDs here
  },
  resourceType: "text",
});
The provenance graph then shows the full chain:
[user: alice] ──creates──► [file.png (CID: Qm...)]

                           inputCid of


[gpt-4o] ──generates──► [response.txt (CID: Qm...)]

Ownership Semantics

Decisionaction.typeEAA meaning
”Yes, I own it”"create"User is the original creator. The resource is a claimed work.
”No, I don’t”"reference"User is citing an external source. Original creator is unknown. The resource is an unclaimed input.
Both cases result in a CID that can be referenced in downstream provenance actions. The difference is the action type and whether a human creator entity is recorded alongside the resource.
Ownership is asserted, not verified. ProvenanceKit records the assertion — trust is established by the completeness and consistency of the provenance graph over time.
For files that already have a 100% match in the system (returned by FileProvenanceTag), no ownership claim is needed — the existing CID should be reused directly as an inputCid.