Contract Tokenization Flow

Written By Catalin Fetean

Last updated 11 months ago

Overview

When two parties—such as a buyer and a seller—want to formalize a deal on Nexity, they create a Contract. This contract specifies the details of the goods or services, the agreed-upon price, and the timelines. Once both sides finalize and sign, the contract becomes the single source of truth for that transaction.

Why It Matters

  • Ensures both parties are on the same page about requirements and obligations.

  • Stores all contract details in one secure place.

  • Optionally provides on-chain reference for added security and transparency (though this happens behind the scenes if your organization needs it).

High-Level Flow

  1. Draft: One party starts a new draft, adding the main details (what’s being sold, price, delivery terms, etc.).

Review: Both parties can check the draft and adjust any terms.

Sign: Each party provides a digital signature to confirm they agree.

Active: Once signed, the contract is considered “live,” and any next steps (like placing orders) can begin.


Step-by-Step: Creating a Contract

  1. Initiate

    • From the main dashboard, click “Create a New Contract.”

    • Fill in key fields like the buyer/seller names and the scope of what’s being contracted.

  2. Add Terms and Documents

    • Include your payment terms (e.g. 50% upfront, 50% on delivery).

    • If needed, attach or reference extra documents (like official specs or special clauses).

  3. Preview

    • Nexity presents a clean summary or document view.

    • Double-check that quantities, prices, timelines, and responsibilities look correct.

  4. Finalize

    • Once satisfied, the contract moves to a Final state.

    • Nexity may generate a PDF or store a reference in the system (and optionally on the blockchain).

  5. Obtain Signatures

    • The buyer and seller (or any designated signers) each confirm agreement by clicking “Sign.”

    • Nexity updates the status to “Active Contract” once everyone signs.


Additional Options

  • On-Chain Tokenization
    If you want an extra layer of security, Nexity can store a digital fingerprint of the contract on a blockchain. This is usually invisible to you—just an extra guarantee that no one can change contract details.

  • Integration with Other Flows
    After a contract is active, you can seamlessly launch orders or initiate payments based on those contract terms. Nexity keeps track of everything, so you don’t have to retype details.


Key Business Benefits

  1. Clarity: You know exactly what was agreed upon, with all details in one document.

  2. Speed: Creating and signing happens in a few clicks—no endless email chains or confusion over the latest version.

  3. Security: Digital signatures and optional blockchain references ensure the contract can’t be tampered with.

  4. Workflow Alignment: Once a contract is in place, it’s easy to trigger the Order Flow or Payment Flow next.

Business-Level Overview

  1. Drafting the Contract

    • A user (buyer or seller) clicks “Create New Contract” in the Nexity dashboard.

    • They enter the main deal details: names of parties, goods/services, price, terms, and any special clauses.

  2. Review & Finalize

    • The contract draft is displayed as a neat document (PDF or HTML).

    • The user checks the information, makes edits if needed, and clicks “Finalize” once everything looks good.

  3. Signatures

    • Both parties (buyer and seller) are prompted to sign. In Nexity, this can be a digital signature prompt.

    • After both parties sign, the contract status changes to Active and can’t be altered without mutual consent.

  4. On-Chain (Optional)

    • If your organization requires blockchain security, Nexity can register a “fingerprint” (hash) of the contract or deploy a specialized smart contract.

    • This ensures the contract terms are tamper-proof and verifiable on a public or private chain.

Technical Details

Database Model

CREATE TABLE contracts (
  id SERIAL PRIMARY KEY,
  contract_number VARCHAR(50) UNIQUE NOT NULL,
  buyer_id INT REFERENCES users(id),
  seller_id INT REFERENCES users(id),
  title VARCHAR(255),
  description TEXT,
  amount NUMERIC(18,2),
  currency VARCHAR(10),
  status VARCHAR(50) DEFAULT 'DRAFT', -- e.g., 'DRAFT', 'PENDING_SIGNATURE', 'ACTIVE', 'COMPLETED', 'CANCELLED'
  on_chain_address VARCHAR(100),      -- if we deploy on chain
  doc_hash VARCHAR(255),             -- hash of the contract document
  buyer_signed BOOLEAN DEFAULT false,
  seller_signed BOOLEAN DEFAULT false,
  created_at TIMESTAMP DEFAULT NOW(),
  updated_at TIMESTAMP DEFAULT NOW()
);

Creating a Draft Contract

Example
// src/routes/contracts.js const router = require('express').Router(); const authMiddleware = require('../middleware/auth'); const { pool } = require('../db'); const crypto = require('crypto'); // Utility function for a contract number function generateContractNumber() { const now = new Date(); return `CONT-${now.getFullYear()}-${crypto.randomBytes(3).toString('hex')}`; } // 1. Create a Draft Contract router.post('/', authMiddleware, async (req, res) => { const { buyerId, sellerId, title, description, amount, currency } = req.body; try { // Contract number generation const contractNumber = generateContractNumber(); // Insert draft into DB const insertQuery = ` INSERT INTO contracts ( contract_number, buyer_id, seller_id, title, description, amount, currency, status ) VALUES ($1, $2, $3, $4, $5, $6, $7, 'DRAFT') RETURNING * `; const values = [contractNumber, buyerId, sellerId, title, description, amount, currency]; const result = await pool.query(insertQuery, values); return res.status(201).json({ message: 'Contract draft created', contract: result.rows[0] }); } catch (error) { console.error(error); return res.status(500).json({ error: 'Internal server error' }); } }); module.exports = router;

Finalizing & Hashing the Document

Example
// 2. Finalize and store doc hash router.post('/:id/finalize', authMiddleware, async (req, res) => { const contractId = req.params.id; // Suppose the front-end sends a base64-encoded PDF or HTML const { contractDocumentBase64 } = req.body; try { // 1) Compute the hash const buffer = Buffer.from(contractDocumentBase64, 'base64'); const docHash = crypto.createHash('sha256').update(buffer).digest('hex'); // 2) Update contract status and store the hash const updateQuery = ` UPDATE contracts SET doc_hash = $1, status = 'PENDING_SIGNATURE', updated_at = NOW() WHERE id = $2 RETURNING * `; const result = await pool.query(updateQuery, [docHash, contractId]); if (result.rowCount === 0) { return res.status(404).json({ error: 'Contract not found' }); } return res.json({ message: 'Contract finalized. Signatures required.', contract: result.rows[0] }); } catch (error) { console.error(error); return res.status(500).json({ error: 'Internal server error' }); } });

Signing the Contract

Example
// 3. Signing the Contract router.post('/:id/sign', authMiddleware, async (req, res) => { const contractId = req.params.id; const userId = req.user.userId; try { // Fetch contract const contractResult = await pool.query( 'SELECT * FROM contracts WHERE id = $1', [contractId] ); if (contractResult.rows.length === 0) { return res.status(404).json({ error: 'Contract not found' }); } const contract = contractResult.rows[0]; // Determine if user is buyer or seller let updateField = null; if (contract.buyer_id === userId) { updateField = 'buyer_signed'; } else if (contract.seller_id === userId) { updateField = 'seller_signed'; } else { return res.status(403).json({ error: 'Not authorized to sign' }); } // Mark as signed await pool.query( `UPDATE contracts SET ${updateField} = true, updated_at = NOW() WHERE id = $1`, [contractId] ); // Check if both have signed const checkResult = await pool.query( 'SELECT buyer_signed, seller_signed FROM contracts WHERE id = $1', [contractId] ); const { buyer_signed, seller_signed } = checkResult.rows[0]; if (buyer_signed && seller_signed) { // Update status to 'ACTIVE' await pool.query( `UPDATE contracts SET status = 'ACTIVE' WHERE id = $1`, [contractId] ); } return res.json({ message: 'Contract signed successfully' }); } catch (error) { console.error(error); return res.status(500).json({ error: 'Internal server error' }); } });

Smart Contract (Solidity)

Example
// SPDX-License-Identifier: MIT pragma solidity ^0.8.17; /** * @title NexityContractRegistry * @dev Simple registry to record the hash of an off-chain contract doc plus buyer/seller addresses */ contract NexityContractRegistry { struct ContractRecord { address buyer; address seller; string docHash; // e.g., a SHA-256 hash of the contract text bool buyerSigned; bool sellerSigned; bool isActive; } // Map a unique ID (e.g. contractNumber hashed) to a ContractRecord mapping(bytes32 => ContractRecord) public contracts; event ContractCreated(bytes32 indexed contractId, address buyer, address seller, string docHash); event ContractSigned(bytes32 indexed contractId, address signer); function createContract(bytes32 _contractId, address _buyer, address _seller, string memory _docHash) public { // Typically restricted by some access control require(contracts[_contractId].buyer == address(0), "Contract already exists"); contracts[_contractId] = ContractRecord({ buyer: _buyer, seller: _seller, docHash: _docHash, buyerSigned: false, sellerSigned: false, isActive: false }); emit ContractCreated(_contractId, _buyer, _seller, _docHash); } function signContract(bytes32 _contractId) public { ContractRecord storage rec = contracts[_contractId]; require(rec.buyer != address(0), "Contract does not exist"); require(!rec.isActive, "Contract is already active"); // If the caller is the buyer or the seller, mark them as signed if (msg.sender == rec.buyer) { rec.buyerSigned = true; } else if (msg.sender == rec.seller) { rec.sellerSigned = true; } else { revert("Not authorized to sign"); } emit ContractSigned(_contractId, msg.sender); if (rec.buyerSigned && rec.sellerSigned) { rec.isActive = true; // both have signed, contract is active } } }

How it ties back:

  1. When you finalize the contract off-chain, you compute its docHash.

  2. Then you call createContract(...) on this registry with contractId = keccak256(...some unique value...) and store docHash in the record.

  3. Buyer/seller can sign on-chain using their wallets. The contract updates the record to isActive once both sign.

  4. Your Node.js backend can store the on_chain_address or the contractId returned by the blockchain in the contracts table.

Best Practices

Versioning & Edits

  • When the contract is in DRAFT, multiple edits are allowed.

  • Once FINALIZED, it’s locked. If changes are needed, you can either “unfinalize” or create an amendment contract.

Notifications & Reminders

  • Each significant step (finalizing, signing) triggers notifications:

    • Email or In-app notification to the other party: “Contract #XYZ is ready for your signature.”

  • This ensures all parties stay updated and no steps get overlooked.

Security & Access Control

  • Only buyer or seller (or authorized delegates) can sign the contract.

  • Admin can override in certain cases if needed (e.g., force-cancel a fraudulent contract).

Integration with Payment & Order Modules

  • Once a contract is ACTIVE, a user can:

    • Launch Orders referencing the contract’s ID (the platform automatically pulls price or quantity from the contract).

    • Initiate Payment (Part 8), possibly referencing the contract for escrow or direct settlement.

Escrow Logic (Optional)

  • If you want to hold funds until the contract terms are met, the same on-chain contract can incorporate an escrow function.

  • Alternatively, the contract references Circle or open banking flows, where the final payment is triggered only after contract completion.