Technical Reference

Grading Company Integration Guide

Everything you need to integrate with the digital-card-twin Protocol and issue verifiable NFT certificates for every graded TCG card.

Architecture

Overview

The digital-card-twin Protocol creates an immutable, on-chain record for every grading certificate issued. When a card is graded:

  1. The grading company notifies digital-card-twin via webhook or API call.
  2. The oracle fetches certificate details, pins metadata to IPFS, and submits a signed transaction to Base (Ethereum L2).
  3. An ERC-721 NFT is minted to the card owner's wallet — the permanent, fraud-proof record.
Grading company
      │
      │  POST /api/v1/webhooks/psa   (webhook push)
      │    — or —
      │  POST /api/v1/certs/batch-submit  (manual / bulk)
      ▼
digital-card-twin API
      │
      │  Lookup cert from PSA API
      │  Upload metadata + image to IPFS
      │  Sign certificate with KMS oracle key (EIP-712)
      ▼
GradingOracleAdapter.sol (Base)
      │
      │  Verify EIP-712 signature
      │  Call DigitalCardTwinRegistry.mintCertificate()
      ▼
ERC-721 NFT minted to recipient wallet

Setup

Getting started

1. Register as a grading company

Before any certificates can be minted, your company must be registered in GradingCompanyRegistry.sol. Contact the digital-card-twin team to complete this one-time on-chain registration. You will receive:

  • Your gradingCompanyId — a bytes32 value (keccak256 of your company name)
  • Your oracle adapter contract address on Base
  • Your API key for the digital-card-twin HTTP API

2. Obtain an API key

API keys are scoped per grading company and authorise write operations. Contact engineering@digital-card-twin.com to request a key for staging and production. Include the key in all write requests:

http
Authorization: Bearer <your-api-key>

Options

Integration paths

PathBest forLatency
Webhook (push)Automated flow — events pushed as cards are gradedNear real-time
Batch submit (pull)Manual backfill, bulk migration, or when webhook is unavailableOn-demand

Recommended

Path 1 — Webhook integration

Step 1 — Configure the webhook endpoint

EnvironmentWebhook URL
Staginghttps://api-staging.digital-card-twin.com/api/v1/webhooks/psa
Productionhttps://api.digital-card-twin.com/api/v1/webhooks/psa

Step 2 — Sign the request (HMAC)

All webhook requests must be signed with the shared PSA_WEBHOOK_SECRET:

python
# Python
import hmac, hashlib, json

secret = b"your-webhook-secret"
body   = json.dumps(payload, separators=(",", ":")).encode()
sig    = "sha256=" + hmac.new(secret, body, hashlib.sha256).hexdigest()
# Include as header: X-PSA-Signature: {sig}
typescript
// TypeScript
import { createHmac } from "node:crypto";

const sig = "sha256=" + createHmac("sha256", secret)
  .update(JSON.stringify(payload))
  .digest("hex");

Step 3 — Send a webhook event

http
POST /api/v1/webhooks/psa
Content-Type: application/json
X-PSA-Signature: sha256=abc123...

{
  "event": "grade.issued",
  "certNumber": "85237194",
  "recipient": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"
}
EventAction
grade.issuedTriggers a digital-card-twin mint
grade.updatedAccepted, no action in v1 (re-mint support in v2)
grade.voidedAccepted, no action in v1 (burn support in v2)

Response: 202 Accepted

json
{
  "jobId": "3f2504e0-4f89-11d3-9a0c-0305e82c3301",
  "status": "queued",
  "certNumber": "85237194"
}

Step 4 — Track the mint job

http
GET /api/v1/jobs/3f2504e0-4f89-11d3-9a0c-0305e82c3301
Authorization: Bearer <your-api-key>
json
{
  "jobId": "3f2504e0-4f89-11d3-9a0c-0305e82c3301",
  "certNumber": "85237194",
  "status": "confirmed",
  "txHash": "0x4a5c89f2...",
  "tokenId": 42,
  "blockNumber": 14523901,
  "updatedAt": "2026-04-10T09:12:04.123Z"
}

Polling strategy: exponential back-off starting at 2 s, capped at 30 s. Typical Base confirmation is 2–4 seconds.

Manual / bulk

Path 2 — Batch submission

http
POST /api/v1/certs/batch-submit
Content-Type: application/json
Authorization: Bearer <your-api-key>

{
  "certs": [
    {
      "certNumber": "85237194",
      "recipient": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"
    },
    {
      "certNumber": "72461038",
      "recipient": "0xAb5801a7D398351b8bE11C439e05C5B3259aeC9B"
    }
  ]
}
  • Maximum 50 certs per request.
  • Submissions are idempotent — the same certNumber submitted twice will not create two NFTs.
StatusMeaning
queuedMint job enqueued successfully
already_mintedNFT already exists — idempotent, no duplicate minted
not_foundCert number not found in PSA
pending_gradeCard graded as AUTH / Pending — not yet a numeric grade
errorPSA lookup or metadata pipeline failure — check error field

Public endpoint

Certificate verification

Anyone can verify whether a cert has been minted — no API key required.

http
GET /api/v1/certs/85237194/status
json
{
  "certNumber": "85237194",
  "status": "minted",
  "tokenId": 42,
  "txHash": "0x4a5c89f2...",
  "blockNumber": 14523901,
  "metadataURI": "ipfs://QmXyz.../metadata.json",
  "mintedAt": "2026-04-10T09:12:04.000Z"
}

Reliability

Error codes & retry guidance

HTTPCodeMeaningAction
401INVALID_SIGNATUREHMAC mismatchRe-check secret; regenerate signature
400(validation)Missing required fieldFix request body
422PENDING_GRADECard not yet assigned a numeric gradeRetry after grading completes
422CERT_NOT_FOUNDCert number not in PSAVerify cert number is correct
429(rate limit)Rate limit exceededBack off and retry (Retry-After header present)
500(internal)Unexpected errorContact support with jobId

Webhook retry policy: exponential back-off at 10 s → 30 s → 2 min → 10 min → 1 hour. After 5 failures the digital-card-twin team is alerted.

Oracle retries: up to 5 automatic retries with exponential back-off (base 2 s). After 5 failures the job enters failed state.

Developer experience

TypeScript SDK

bash
npm install @digital-card-twin/sdk
typescript
import { DigitalCardTwinClient } from "@digital-card-twin/sdk";

const client = new DigitalCardTwinClient({
  apiUrl: "https://api.digital-card-twin.com",
  subgraphUrl: "https://api.studio.thegraph.com/query/<ID>/...",
  rpcUrl: "https://base-sepolia.g.alchemy.com/v2/<KEY>",
});

// Look up a certificate
const result = await client.lookupCertificate("85237194", "PSA");
if (result.found) {
  console.log(result.tokenId, result.owner);
}

// Submit a grading event (requires API key)
const job = await client.submitCertificate(cert, process.env.DCT_API_KEY);
if (job.queued) {
  console.log("Job ID:", job.jobId);
}

Full documentation: @digital-card-twin/sdk on npm ↗

Network

On-chain contract addresses

ContractBase Sepolia (staging)Base Mainnet
DigitalCardTwinRegistry0xad3898d76af2024ee1ebd2f5ff10fdeefe64361bTBD
PSA GradingOracleAdapter0x88caa4def2ada553b44e59d633208474447886d1TBD

Contract ABIs and deployment details are in the contracts/ directory of the monorepo. Registry contracts are verified on Basescan.

Links

Additional resources