Your company states that a supplier passed a check, an audit or a certification, until a date. The statement is an attestation on a public network: your partners and auditors verify it without asking you, and if the supplier stops meeting the bar, you revoke it and everyone sees that too.
Recorded from the real console on a sandbox over a fork of a public test network where nothing has value: an accreditation attested, verified, and revoked after approval.
The system that records a passed check sends the attestation to CryptoRail. The attestation policy reads the call itself: the company's attester key may attest only under the supplier accreditation schema, so it cannot be used to state anything else. Withdrawing an accreditation waits for a reviewer. Every attestation and revocation comes back to your systems as a signed notification carrying its identifier.
flowchart TD
BO[Your back office records a passed check] --> P{Attestation policy}
P -->|another schema| X[Refused with the reason, nothing sent]
P -->|the accreditation schema| S[Signed with the attester's own key]
S --> E[Attestation on chain: identifier and evidence hash]
E --> H[Signed webhook with its UID]
E --> V[Anyone verifies: valid, revoked, expired]
W[Your back office withdraws it] --> A[A reviewer approves]
A --> R[Revocation on chain, signed webhook]An attestation can be revoked, but it can never be erased: anyone can read it, and its history, for as long as the network exists. So the pack attests only an internal identifier of the supplier, the name of the check, and the hash of the evidence's reference, never personal data or documents. The audit report stays in your systems; whoever holds its reference can show that it matches the hash on chain.
The pack's encoder refuses anything that is not an identifier, such as a person's name or an email address.
flowchart LR
Y[Your records: supplier file, audit report] --> K[Kept in your systems]
Y -->|internal id, check name, hash of the report's reference| C[On chain, public for good]
C --> Z[Anyone holding the reference can show it matches]The pack connects the Ethereum Attestation Service that already runs on the network; there is no contract of ours to trust.
EAS and its schema registry where the network places them, checked against the chain before use. See it in code ↓
A supplier identifier, the check passed and the evidence's hash, with an expiry and a revocation. See it in code ↓
The attester key attests nothing else, and a revocation waits for a reviewer. See it in code ↓
Valid, revoked, expired or unknown, read from the chain, by you or by anyone. See it in code ↓
A retry never attests twice, a refusal always says why, and every withdrawal is recorded with who approved what and when.
On every change, an end-to-end scenario runs the pack against the real EAS and schema registry on a fork of Base Sepolia at a pinned block: the schema registered once, an accreditation attested, delivered and verified, another schema refused, a revocation approved, an expiry reached. The scenario and the requirements behind it are listed on the evidence page. On 2026-10-12 the same pack ran on Base Sepolia itself; anyone can check its attestations on EAS's own explorer, without asking us.
| On Base Sepolia | Outcome | Transaction | Attestation |
|---|---|---|---|
| The schema registered, once for good | Confirmed; a second setup found it and sent nothing | 0x090b3bc4…49a0e2 | none |
| A quality audit attested for a listed supplier | Confirmed; delivered as a signed webhook, read as valid | 0x53cb71b4…7405b0 | 0xb76ba006…1cba1b |
| An attestation under another schema | Refused by the policy; nothing sent | none | none |
| That audit revoked, with a reason | Held until the reviewer approved; then confirmed, read as revoked | 0xe3679115…a718de | 0xb76ba006…1cba1b |
| An ISO 9001 certificate attested, valid until 2027-10-12 | Confirmed; left valid for anyone to check | 0x421b5749…c37d36 | 0x8dcc9a14…8dbf6d |
The attester's address is 0xd9d057ab…488f01; the schema is 0xd1f2429d…daf8ac. The suppliers are demo addresses nobody holds a key for.
The pack is a small library over CryptoRail's API: a manifest of EAS and its schema registry verified against the chain, the schema, the attestation policy for the service's rules engine, and a setup that registers everything by name. Your systems then use the typed SDK, or plain HTTP. The calls below are the ones the scenario makes on every change.
Three fields, encoded with the same ABI rules as any contract call. The schema's UID is a hash of its text, so the policy can name it before it is registered anywhere.
import {
SUPPLIER_ACCREDITATION_SCHEMA,
SUPPLIER_ACCREDITATION_UID,
encodeAccreditation,
evidenceHash,
} from '@cryptorail/pack-attestations'
SUPPLIER_ACCREDITATION_SCHEMA
// 'string supplierId,string accreditation,bytes32 evidenceHash'
// revocable, no resolver; the expiry is the attestation's own
SUPPLIER_ACCREDITATION_UID
// keccak256(schema, resolver 0x0, revocable): the same on every chain
// '0xd1f2429d9ded9746094cc4a4b6819260aeea2b7ccb52fdc61eaffeac25daf8ac'
const data = encodeAccreditation({
supplierId: 'SUP-00042', // your internal id, never a name
accreditation: 'quality-audit-2026',
evidenceHash: evidenceHash('AUD-2026-0042'), // the report stays with you
})
// encodeAccreditation({ supplierId: 'Jane Doe', … }) throws:
// what is attested is public for goodVerify that the network's EAS is the one the pack knows, print the policy, and register everything on the service. The schema is read first and registered through the API only when the chain does not have it: never twice.
# Check EAS and the schema registry against the chain, at the pinned block: # code hashes of proxy and implementation, version, every selector node packages/pack-attestations/dist/bin.js verify --network base-sepolia # Print the attestation policy for the service's configuration node packages/pack-attestations/dist/bin.js policy --network base-sepolia \ --approvers <approver id> # Register EAS and the registry by name, the schema if the chain lacks it, # the back office's webhook and the Attested and Revoked subscriptions CRYPTORAIL_API_KEY=<admin key> \ node packages/pack-attestations/dist/bin.js setup --network base-sepolia \ --url <service URL> --registrar <attester identity id> --webhook <back office URL> # First run: schema "registered", with its transaction. Any later run: "found"; # nothing is registered twice
The rules read inside the call: the schema is a field of the struct EAS takes, and the policy reads it by its path, request.schema. A call the rules cannot read is refused, never let through.
import { BASE_SEPOLIA, attestationPolicy } from '@cryptorail/pack-attestations'
const policy = attestationPolicy({
version: 'accreditations-2026-10', // recorded with every decision
network: BASE_SEPOLIA,
approverIds: [reviewerId], // who lets a revocation through
})
// What it says, as the rules engine reads it:
// deny attest, revoke when request.schema is not ours
// deny register unless it is our schema, no resolver, revocable
// deny raw calls or ether to EAS and the registry
// approve revoke 1 of the reviewers
//
// adapters: { policy: { kind: 'rules', options: policy } }One call, by function name, with the attester's own identity. The UID of the attestation arrives in the signed webhook of its Attested event.
import { createClient } from '@cryptorail/sdk'
import { attestRequest } from '@cryptorail/pack-attestations'
const cryptorail = createClient({ baseUrl, apiKey })
const { data } = await cryptorail.POST('/v1/transactions', {
params: { header: { 'idempotency-key': `accreditation-${checkId}` } },
body: {
chainId: 84532, // Base Sepolia
actor: { kind: 'identity', identityId: attesterId }, // a key of its own
target: {
kind: 'contract',
contractId: easId, // 'pack-eas', registered by the setup
function: 'attest',
args: [attestRequest({
recipient: supplierAddress, // from your list of suppliers
expirationTime: validUntil, // seconds; 0n for never
accreditation: { supplierId: 'SUP-00042',
accreditation: 'quality-audit-2026',
evidenceHash: evidenceHash('AUD-2026-0042') },
})],
},
},
})// The same key, another schema: refused before anything is signed
{
"state": "REJECTED_BY_POLICY",
"failure": {
"stage": "policy",
"cause": "policy.operation-denied",
…
}
}// In the back office's webhook endpoint, after verifyWebhook(...) passes for (const event of JSON.parse(rawBody).events) { // event.type: 'contract.event' // event.payload: { contractLabel: 'pack-eas', // eventName: 'Attested' | 'Revoked', // params: { recipient, attester, uid, schemaUID }, … } // params.uid is the attestation's identifier: store it with the check }
A read through CryptoRail, judged at a time. EAS's own validity check only says that an identifier exists, so the pack reads the record and its revocation and expiry.
import { attestationValidity } from '@cryptorail/pack-attestations'
// getAttestation(uid) on EAS through CryptoRail: a read, nothing signed
const { status, accreditation } = await attestationValidity(
cryptorail, { contractId: easId }, uid, BigInt(Math.floor(Date.now() / 1000)),
)
// status: 'valid' | 'revoked' | 'expired' | 'unknown'
// accreditation: { supplierId, accreditation, evidenceHash }
// Anyone else checks the same record without us, on any node or on
// https://base-sepolia.easscan.org/attestation/view/<uid>Withdrawing a public statement waits for a reviewer, who signs the typed data the service serves, exactly as an approver of a payment does.
Possible next steps, not part of the pack: attestations signed off chain (EIP-712), delegated attestation, and a resolver contract that checks each attestation on chain.
import { revokeRequest } from '@cryptorail/pack-attestations'
const { data } = await cryptorail.POST('/v1/transactions', {
params: { header: { 'idempotency-key': `withdraw-${checkId}` } },
body: {
chainId: 84532,
actor: { kind: 'identity', identityId: attesterId },
target: { kind: 'contract', contractId: easId,
function: 'revoke', args: [revokeRequest(uid)] },
},
})
// state: PENDING_APPROVAL until a reviewer signs what the decoded call says
// (POST /v1/approvals/{transactionId}/signatures); then CONFIRMED,
// a signed Revoked webhook with the same uid, and the status reads 'revoked'We show the pack on our sandbox, on a public test network where nothing has value. A pilot gives you your own:A pilot environment stood up in minutes with nothing installed on your side.