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| Contract | Address | Deployed |
|---|---|---|
| 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| Contract | Address | Deployed |
|---|---|---|
| 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
0x82287f6355a79c8cf00b3c52090128ae7f13b61c65c95ae1c319910377054cc8keccak256(eip155:1:0x43415eb6ff9db7e26a15b704e7a3edce97d31c4e)- Disclosure freshness
0xa40f966944356ce543320f59f22ba003e3f0be28532bae029d4f9f9d05efc589keccak256(eip155:1:0x43415eb6ff9db7e26a15b704e7a3edce97d31c4e#policy:disclosure-freshness:1)- NAV settlement
0x3cd89b1b73d40adc47c25a1fbf2123dcf6839c92a446275db8f9268a146b62b8keccak256(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.