Touchstone

Developers

By the contracts, for your contracts

One function, one answer. check(bytes32) returns (bool allowed, string reason) — the same call from Solidity, from TypeScript, or from a raw eth_call, on X Layer mainnet and testnet. Adding a policy check to a contract you already have is a few lines and one address.

Quick start

Three ways in

Pick the layer you already work at. All three read the same contracts and get the same answer.

1 · Solidity

The interface is the whole dependency. Both gate generations answer this exact selector, so a consumer written against the interface works unchanged when its gate is upgraded from the v1 registry to the v2 — which is why it imports the interface rather than a concrete gate type.

contracts/ITouchstoneGate.sol

// SPDX-License-Identifier: Apache-2.0
pragma solidity ^0.8.24;

interface ITouchstoneGate {
    function check(
        bytes32 assetKey
    ) external view returns (bool allowed, string memory reason);
}

And the consumer — the pattern the deployed GuardedAction contracts use, unchanged: ask the gate, revert with the gate's own reason string when it refuses, proceed when it permits. The gate address and the asset key are immutable constructor arguments, so a consumer cannot be quietly repointed after deployment.

your contract

ITouchstoneGate public immutable gate;
bytes32 public immutable assetKey;

function execute() external {
    (bool allowed, string memory reason) = gate.check(assetKey);
    if (!allowed) revert ActionRefused(assetKey, reason);
    // ... the action the policy guards
}

2 · TypeScript — the SDK

The SDK is published as touchstone-sdk on the public npm registry, built from this repository's sdk/ workspace. Building from a checkout stays supported and is what CI runs.

install

npm install touchstone-sdk ethers

# or from a checkout of this repository
git clone https://github.com/Ridwannurudeen/touchstone
cd touchstone/sdk
npm ci && npm run build && npm test

check, then act

import { JsonRpcProvider, Wallet } from "ethers";
import {
  AssetGateClient,
  DEPLOYMENTS,
  GuardedActionClient,
  POLICIES,
} from "touchstone-sdk";

const deployment = DEPLOYMENTS.xlayerMainnet;   // .xlayerTestnet for chain 1952
const provider = new JsonRpcProvider(rpcUrl, deployment.chainId);
const wallet = new Wallet(privateKey, provider);

// gate address: the AssetGate row for your chain in the table below
const gate = new AssetGateClient(
  gateAddress,
  POLICIES.disclosureFreshness.registryKey,
  wallet
);
const action = new GuardedActionClient(guardedActionAddress, wallet);

const decision = await gate.check();   // { allowed, reason }
if (decision.allowed) await action.execute();

AssetGateClient.demand() exercises the gate's state-changing path and rejects a known refusal before it submits anything. GuardedActionClient.execute(builderCodes) appends the canonical ERC-8021 attribution suffix when you pass a registered Builder Code. Touchstone's live Terminal uses f0axgs7smtk2nfa7; its first attributed mainnet execution is public. Integrators should register and pass their own code.

read the registry directly

import { RegistryV2Client, indexPublished, policyRegistryKey } from "touchstone-sdk";

const registry = new RegistryV2Client(registryAddress, provider);
const report = await registry.latestReport(POLICIES.navSettlement.registryKey);
// null until that key has a publication; otherwise reportDigest, policyId,
// policyRoot, controlSetRoot, evidenceRoot, approvalDigest, epochKey, status,
// observedAt, validUntil, publisher, sequence, parentDigest and reportURI.

const events = await indexPublished(provider, registryAddress, fromBlock, "latest");
// Published and Corrected, in canonical log order. Handle both kinds: a
// correction must not leave cached permissive state behind.

// Derive a key rather than hand-hashing one.
policyRegistryKey(POLICIES.navSettlement.assetKey, "nav-settlement", 1);

3 · Raw JSON-RPC — no dependencies at all

The selector is the first four bytes of keccak256("check(bytes32)"). Compute it rather than trusting it printed here — cast sig 'check(bytes32)', or keccak256(toUtf8Bytes("check(bytes32)")).slice(0, 10) in ethers. The calldata is that selector followed by the 32-byte registry key.

eth_call · X Layer mainnet

SELECTOR=399e0792                 # keccak256("check(bytes32)")[0:4]
KEY=a40f966944356ce543320f59f22ba003e3f0be28532bae029d4f9f9d05efc589
GATE=0xAac48DC261B04737FDCB101D5049395121034a83

curl -s https://rpc.xlayer.tech -H 'content-type: application/json' -d "{
  \"jsonrpc\":\"2.0\", \"id\":1, \"method\":\"eth_call\",
  \"params\":[{\"to\":\"$GATE\",\"data\":\"0x$SELECTOR$KEY\"},\"latest\"]
}"

The result is the ABI encoding of (bool allowed, string reason): the boolean in the first word, then the offset, length and bytes of the reason string. Against the disclosure-freshness key this gate answers (true, "allowed") as of the most recent publication window — the Policy Terminal reads it live rather than reprinting it. When a gate refuses, the reason is one of its own constants: unknown asset, status not allowed, observation too old, wrong publisher, control-set mismatch.

Deployments

Network addresses

Live on X Layer mainnet and testnet. Every address below links to the explorer for its own chain.

Pin the chain id. Always. Several of these addresses exist on both chains as entirely different contracts: one deployer replayed the same CREATE nonces on mainnet and testnet, so a single address can be a live registry on one chain and a superseded contract — or a guarded action — on the other. An address alone does not identify a deployment here. That is exactly why this section is split per chain and never merged into one list.

X Layer mainnet

CHAIN ID 196
Touchstone contract and role addresses on X Layer mainnet, chain id 196
ContractAddressDeployed
Registry v1Publisher-authenticated commitments 0xc9d58e4496bF061C3177301Ff02518eBB70AD30d block 68291416
Registry v2EIP-712-attested reports; a relayer submits without being the reporting authority 0x0dAb4A5B7dd24434Ab6564734E26d3d76985352C block 68389940deploy transaction
AssetGatePinned to the disclosure-freshness policy key 0xAac48DC261B04737FDCB101D5049395121034a83 block 68389983
AssetGateV2Pins policy, control-set root and approval digest 0x8641CF6d40524AC55aBd0a02601AfBd374EFB059 block 68427105
RWAAdmissionControllerActivates and executes only on the gate's answer 0x5C5265392701A99cbB137aF8116E0F97f630329A block 68427148
GuardedAction — permittedThe consumer whose gate answers allowed 0xBaE680e671e0451b95c9b09eD15F70C3E1EA7720 block 68390049
GuardedAction — refusedThe identical consumer behind a gate that refuses; the pair is the control 0x8FbcFf50bf1F88cADEc9103a57c4C86e8A44BAcB block 68390055
PublisherRole — the reporting authority recovered on chain 0x86A100BDdF8754c95fec97BeC96dBFd64Be44710
RelayerRole — pays gas for v2 publications, attests nothing 0x5b4e381CA8CCFF91553711512f08B4dF685faFCe

X Layer testnet

CHAIN ID 1952
Touchstone contract and role addresses on X Layer testnet, chain id 1952
ContractAddressDeployed
Registry v1Publisher-authenticated commitments 0x0dAb4A5B7dd24434Ab6564734E26d3d76985352C block 38489602
Registry v2EIP-712-attested reports; a relayer submits without being the reporting authority 0xBaE680e671e0451b95c9b09eD15F70C3E1EA7720 block 38699818deploy transaction
AssetGatePinned to the disclosure-freshness policy key 0x0bc5c0cc879CE1b5AD23aEdA8fC42dB414eB8eE1 block 38699975
AssetGate — first generationPinned to the asset key rather than a policy key; kept live as the earlier generation 0xAac48DC261B04737FDCB101D5049395121034a83 block 38602126
AssetGateV2Pins policy, control-set root and approval digest 0xE1e2C897A43674bba6c3fbE6584a703a09939930 block 38876261
RWAAdmissionControllerActivates and executes only on the gate's answer 0x1822Cde72cD1aB560d8fdD795Ac6971b122BbA28 block 38876477
GuardedAction — permittedThe consumer whose gate answers allowed 0xf6D53a9cD76C6777835a6b4070e88337199127Dc block 38700019
GuardedAction — refusedThe identical consumer behind a gate that refuses; the pair is the control 0x8641CF6d40524AC55aBd0a02601AfBd374EFB059 block 38700025
PublisherRole — the reporting authority recovered on chain 0x86A100BDdF8754c95fec97BeC96dBFd64Be44710
RelayerRole — pays gas for v2 publications, attests nothing 0x5b4e381CA8CCFF91553711512f08B4dF685faFCe

The publisher and relayer are the same identities on both chains; the contracts are not. Endpoints: https://rpc.xlayer.tech for chain 196, https://testrpc.xlayer.tech/terigon for chain 1952. Confirm an endpoint's own eth_chainId before you send anything to it.

Keys

Every contract above is source-verified on OKLink — open an address and its #code tab shows the recompiled source and constructor arguments matching the chain. Start with mainnet Registry v2 or testnet Registry v2; the full list is on Security.

Registry keys, and where they come from

A registry key is keccak256 of a text identifier — nothing else. Derive it, don't copy it. The asset publishes under its own key; each policy publishes under a key of its own, so a policy verdict can never be mistaken for an asset-wide one.

Asset
0x82287f6355a79c8cf00b3c52090128ae7f13b61c65c95ae1c319910377054cc8 keccak256(eip155:1:0x43415eb6ff9db7e26a15b704e7a3edce97d31c4e)
Disclosure freshness
0xa40f966944356ce543320f59f22ba003e3f0be28532bae029d4f9f9d05efc589 keccak256(eip155:1:0x43415eb6ff9db7e26a15b704e7a3edce97d31c4e#policy:disclosure-freshness:1)
NAV settlement
0x3cd89b1b73d40adc47c25a1fbf2123dcf6839c92a446275db8f9268a146b62b8 keccak256(eip155:1:0x43415eb6ff9db7e26a15b704e7a3edce97d31c4e#policy:nav-settlement:1)

In the SDK: registryAssetKey(text) for the asset key, policyRegistryKey(assetKey, policyId, version) for a policy key, and POLICIES.disclosureFreshness.registryKey / POLICIES.navSettlement.registryKey for the two already derived. A shared vector under sdk/fixtures/ locks these derivations to the Python publisher, so the two implementations cannot drift apart.

Freshness

Reports expire. That is the product.

Every report carries observedAt and validUntil. validUntil is the end of the day, UTC, on which that report's evidence deadline falls: a verdict does not outlive the evidence it was drawn from. A fresh report is published at the daily window and the sequence advances.

Gates enforce more than expiry. Each one carries an immutable maxObservationAge and refuses an observation older than it, independently of validUntil; it may additionally pin the publisher (requiredPublisher) and the exact approved control set (requiredControlSetRoot). A consumer inherits every one of those guarantees without implementing any of them.

Treat (allowed, reason) as the complete answer. Do not re-derive a verdict from the report fields, and do not cache a positive answer across the publication window — the call is cheap, and the gate is the only thing that has seen every pin. When it refuses, the reason string names which check failed, and that string is safe to surface to a user or log verbatim.

Correction is first-class, not an exception. Registry v2 emits Corrected alongside Published, and an indexer that handles only the latter can hold permissive state the record has already withdrawn. indexPublished() returns both kinds in canonical log order for exactly that reason.

Verification

Check the answer without trusting this site

Every published report ships as a signed bundle: the Ed25519 report signature, the approval ledger and its digest, the policy manifest, the compilation artifacts and the evidence captures the verdict cites. Nothing in a bundle depends on this host being honest, or on it being online at all.

The verify page walks the checks one at a time, with the commands to run them yourself. The Policy Terminal does the same work in your browser: drop a bundle in and the signature, digest, manifest and attestation checks run locally in the tab — nothing is uploaded, and the panel also states what it does not check.

One call. One answer. Two chains.

Read the live gates in the Terminal, then wire the same call into your contract, your service or your agent.