Integrating Trust: A Developer's Guide to the Resume Protocol
Source: Dev.to
Resume Integrator – A Reference Implementation
In my previous article I introduced the Resume Protocol: a system designed to make professional reputation verifiable, soul‑bound, and owned by you.
But a protocol is only as useful as the tools we build to interact with it.
To bridge the gap between complex smart contracts and everyday utility, I built the Resume Integrator. This isn’t just a script; it’s a reference implementation that demonstrates reliability and excellence in Web3 engineering.
Whether you are building a freelance marketplace or a university‑certification portal, the challenge remains the same: linking rich off‑chain evidence (PDFs, images) with on‑chain truth (immutable ledgers).
In this guide I will walk you through the thoughtful architectural decisions behind this integration.
The Engineering Challenge
| Layer | Description |
|---|---|
| Evidence (off‑chain) | Detailed descriptions, design portfolios, certificates, etc. – heavy data that is inefficient to store on‑chain. |
| Truth (on‑chain) | Cryptographic proof of ownership and endorsement. |
My goal with the Resume Integrator was to stitch these together seamlessly, creating a system that is robust and user‑centric.
Architecture Overview
Step 1 – Structuring Data with Intent (Metadata)
We follow the ERC‑721 Metadata Standard and enforce it with strict TypeScript interfaces. We don’t guess the shape of our data; we define it.
// src/types.ts
export interface Attribute {
trait_type: string;
value: string | number | Date;
}
/**
* Standard ERC‑721 Metadata Schema
* Strict typing ensures every credential we mint
* is readable by standard wallets.
*/
export interface CredentialMetadata {
name: string;
description: string;
image: string;
attributes: Attribute[];
}
Step 2 – The Storage Layer (Pinata SDK)
We use IPFS via the Pinata SDK. Pinata gives us the reliability of a managed service without compromising the decentralized nature of content addressing.
The “Two‑Step” pattern below guarantees data integrity:
- Upload the visual proof first.
- Embed that proof’s URI into the metadata.
// src/storage.ts
/**
* Creates NFT‑compatible metadata for a credential
* and uploads it to IPFS via Pinata.
*
* This function optionally uploads an image first,
* then embeds its IPFS URL into the metadata JSON.
*
* @param input Credential metadata fields
* @returns A public IPFS gateway URL pointing to the metadata JSON
*/
export async function createCredentialMetadata(
input: CredentialMetadataInput
): Promise<string> {
console.log('Authenticating with Pinata...');
await pinata.testAuthentication();
console.log('Pinata authentication successful');
// Will store the IPFS URL of the uploaded image (if any)
let image = '';
// If an image path is provided, upload the image to IPFS first
if (input.imagePath && fs.existsSync(input.imagePath)) {
console.log(`Uploading image: ${input.imagePath}`);
// Read the image file from disk into a buffer
const buffer = fs.readFileSync(input.imagePath);
// Convert the buffer into a File object (Node 18+ compatible)
const file = new File([buffer], 'credential.png', {
type: 'image/png',
});
// Upload the image to Pinata's public IPFS network
const upload = await pinata.upload.public.file(file);
// Construct a gateway‑accessible URL using the returned CID
image = `https://${CONFIG.PINATA_GATEWAY}/ipfs/${upload.cid}`;
console.log(` Image URL: ${image}`);
} else if (input.imagePath) {
console.warn(
`Warning: Image path provided but file not found: ${input.imagePath}`
);
}
// Construct ERC‑721 compatible metadata JSON
const metadata: CredentialMetadata = {
name: input.skillName,
description: input.description,
image,
attributes: [
{ trait_type: 'Recipient', value: input.recipientName },
{ trait_type: 'Endorser', value: input.issuerName },
{
trait_type: 'Date',
value: new Date(
input.endorsementDate.toISOString().split('T')[0]!
),
},
{ trait_type: 'Token Standard', value: 'Soulbound (SBT)' },
],
};
// Upload the metadata JSON to IPFS
console.log('Uploading metadata JSON...');
const result = await pinata.upload.public.json(metadata);
// Return a public gateway URL pointing to the metadata
// This URL can be used directly as a tokenURI on‑chain
return `https://${CONFIG.PINATA_GATEWAY}/ipfs/${result.cid}`;
}
Step 3 – The Issuance Layer (Viem)
I chose Viem because it is lightweight, type‑safe, and aligns with my preference for precision over bloat.
The most critical engineering decision here is waiting for confirmation. Broadcasting a transaction is not enough; we must ensure it is finalized. This prevents UI glitches and guarantees the user knows exactly when their reputation is secured.
// src/contract.ts
/**
* Mint a new endorsement on‑chain
*/
export async function mintEndorsement(
recipient: string,
skill: string,
dataURI: string
): Promise<string> {
if (!CONFIG.CONTRACT_ADDRESS) {
throw new Error('Contract address not set in .env');
}
console.log(`Minting endorsement for ${skill}...`);
const hash = await walletClient.writeContract({
address: CONFIG.CONTRACT_ADDRESS,
abi: CONTRACT_ABI,
functionName: 'endorsePeer',
args: [recipient, skill, dataURI],
});
console.log(` Tx Sent: ${hash}`);
// Wait for confirmation
const receipt = await publicClient.waitForTransactionReceipt({ hash });
console.log(`Confirmed in block ${receipt.blockNumber}`);
return hash;
}
Step 4 – Verification (The Read)
Querying a blockchain state variable one‑by‑one is slow and expensive. Instead, we use event logs. By listening to the EndorsementMinted event, we can reconstruct a user’s entire professional history in a single, efficient query. This approach respects both the network and the user.
That’s it! The Resume Integrator demonstrates a clean, production‑ready way to bind off‑chain evidence to on‑chain reputation, while keeping the developer experience pleasant and the end‑user experience seamless. Feel free to fork the repo, experiment, and build the next generation of credential‑driven applications.
src/contract.ts
/**
* Retrieves all endorsements for a given user address.
*
* @param userAddress - The address of the user whose endorsements are being queried.
* @throws Will throw an error if the contract address is not defined in the environment.
* @returns An array of endorsement objects containing tokenId, skill, issuer, and status.
*/
export async function getEndorsementsFor(userAddress: string) {
if (!CONFIG.CONTRACT_ADDRESS) {
throw new Error('Contract Address not set in .env');
}
console.log(`Querying endorsements for ${userAddress}...`);
const logs = await publicClient.getLogs({
address: CONFIG.CONTRACT_ADDRESS,
event: parseAbiItem(
'event EndorsementMinted(uint256 indexed tokenId, address indexed issuer, address indexed recipient, bytes32 skillId, string skill, uint8 status)'
),
args: {
recipient: userAddress as Hex,
},
fromBlock: 'earliest',
});
return logs.map((log) => ({
tokenId: log.args.tokenId,
skill: log.args.skill,
issuer: log.args.issuer,
status: log.args.status === 1 ? 'Active' : 'Pending',
}));
}
Conclusion
Resume Integrator is more than a codebase—it’s a blueprint for building with purpose.
Separation of concerns
- IPFS – stores heavy data.
- Blockchain – provides trust.
Benefits
- Efficient, immutable, and scalable system.
- Strict typing and confirmation‑waiting ensure reliability for users.
The Resume Protocol is the foundation; the Integrator is the bridge. Now it’s up to you to build the interface.
Repositories
- The Protocol (Smart Contracts)
- The Integrator (Sample Client)
Let’s build something you can trust with clarity, purpose, and excellence.