Why declare anything?
Without a declaration, Touchmark can only ask whether the code is dangerous in general. With one, it can ask whether the code does what you said it does, and that question has sharp answers.
It states the authority
The network, whether the agent holds a key and where it comes from, what it signs, how much it may pay and to whom, and who approves it. In one place, in one shape.
The code is held to it
Testnet only? Then no mainnet passphrase in the code that ships. An allow-list? Then one must be enforced where the agent pays. A broken claim is reported as a risk.
The badge means something
A credential stamped on an honest declaration tells the next person exactly what was promised and checked. It also names the owner who receives the credential.
Where it lives
Commit a file named exactly .tmark at the repository root, next to README.md. When a .tmark exists, older touchmark.json, agentveritas.json and veritas.json files are ignored.
Start from your code
$ python scripts/tmark.py init path/to/repo
# writes .tmark, pre-filled from what the code can do
$ python scripts/tmark.py check path/to/repo
# lists every error; an invalid file is ignored
Check it on every push
- uses: furkan3152/Touchmark_agent@main
with:
fail-on: needs_fixes
annotate: medium
The GitHub Action validates the .tmark, annotates each error on the file, and reports tmark: valid, invalid or missing.
Validate as you type
{
"$schema": "https://usetouchmark.app/schemas/tmark-v1.json",
"tmark": "1.1", …
Editors that read $schema (VS Code, JetBrains) check the file against the schema as you write.
Build one here
Fill in the form; the file on the right is written as you go and checked against the same rules Touchmark uses. Nothing leaves your browser until you press Check with Touchmark.
Field reference
Every field the format has, from tmark-v1.json. Strings are trimmed; amounts are decimal strings with at most 7 decimal places; assets are XLM (or native), CODE:G…ISSUER, or a C… token contract, so a look-alike asset cannot satisfy a limit. Every G… and C… address must pass its checksum.
(top level)
$schemaoptionalThe schema URL, for editors. Any string.
tmarkrequiredThe string
"1"or"1.1". A file that uses any 1.1 field must say"1.1"; every"1"file stays valid unchanged. The builder writes"1.1"only when you use one.agentrequiredWhat the agent is. See below.
authorityrequiredKeys and signing.
limitswhen it moves fundsRequired when
authority.signshaspaymentsor any tool hasmoves_funds: true.owner·tools·forbidden·entrypoints·contactoptionalSee below. Any other top-level field is an error.
agent
namerequired2 to 80 characters.
purposerequired20 to 1000 characters: what the agent does and for whom. This is the intent the code is judged against.
kindrequiredmcp_server,autonomous_agent,x402_client,x402_server,sdk,skill,contractorother.networkrequiredtestnet,mainnetorboth.testnetwith the public network's passphrase or endpoints in shipped code, or with a tool argument, a paid server's challenge or a fallback able to pick mainnet, is a high network mismatch.mainnetwhen the code only ever names testnet is a low one.discloses_aioptional · default trueUsers are told they are talking to an AI.
authority
holds_keysrequiredBoolean: the agent itself holds a Stellar secret key.
falsewhile the code loads a key or signs is a high authority mismatch.key_sourcerequiredenv,kms,external_signer,wallet_promptornone, and since 1.1caller_suppliedandagent_generated. Must agree withholds_keys:env,kmsandagent_generatedneedtrue;noneandwallet_promptneedfalse.1.1
caller_supplied: the caller passes a secret in with each call and the agent keeps none, soholds_keys: falseis honest even though the code loads a key.agent_generated: the agent creates a keypair and stores it itself (a keystore, a file, a database). Withcaller_supplied, the path the secret takes through the model and the tools is still traced by the data-flow rules.signsoptional · default []Up to six, no repeats:
payments,contract_calls,account_changes,trustlines,foreign_transactions,messages. Code that can sign payments, contract calls, account changes or foreign transactions not listed here is a high authority mismatch.agent_accountoptionalThe
G…account orC…contract the agent acts from, if fixed. Read from the ledger: signers, thresholds, balances, or the contract. If every payment needs a co-signer, the check ledger counts that as an enforced limit and approval, which no code change or prompt can remove.
limits
per_payment·per_dayoptional{"amount": "25", "asset": "XLM"}: a positive amount with at most 7 decimal places, and an asset. No amount cap in the code that pays, or no running total near it, is a high “limit not enforced”.recipientsrequiredallowlist(only saved payees),any(whatever the request names) ornone(never pays; an error if it moves funds).allowlistwith no allow-list enforced in code is a high “limit not enforced”.human_approvalrequiredalways,above_limitornever.alwaysorabove_limitwith no approval gate in the code that pays is a high “limit not enforced”.
tools[]
namerequired1 to 80 characters of
A–Z a–z 0–9 _ . : -. Unique across tools.descriptionrequired8 to 400 characters.
moves_funds·signs·networkrequiredBooleans. A tool that signs needs a non-empty
authority.signs; a tool that moves funds needslimits.filesoptional · default falseReads or writes local files: a spend ledger, a keystore, a cache.
executes_codeoptional · default false1.1The tool runs a shell command or code: a build, a CLI, an interpreter. Declared execution is disclosed in the report, not excused: every path from input to the command is still assessed.
generatedoptional1.1This entry stands for a family of tools the code creates at run time:
{"pattern": "invoke_*", "source": "each function of a contract the caller registers"}.patternis a glob of 1 to 120 characters (A–Z a–z 0–9 _ . : - * ? [ ] { }) that every generated name matches;source(8 to 300 characters) says what they are generated from. A tool the code registers whose name matches the pattern counts as declared, and is held to this entry's flags.max_amountoptionalA money cap for this tool, only when
moves_fundsis true.
owner · forbidden[] · entrypoints[] · contact
owner.stellar_accountrequired in ownerA valid
G…account. The credential is issued to it after it signs the registration in Freighter.forbidden[]up to 30Each 8 to 300 characters. Used as goals the agent must refuse.
entrypoints[]up to 10{"path": "src/server.ts", "symbol": "handleRequest"}: a relative path inside the repository (no.., no//) and an identifier. Marks where outside input enters, for the data-flow analysis.contact.securityrequired in contactAn email or URL, 5 to 200 characters. Shown in the report.
Worked examples
Five honest declarations, each valid against the current schema. The MCP server and the SDK describe real Stellar code; the MCP server and the contract tools use 1.1 fields.
A read-only agent
The smallest valid file: three required objects. It holds no key and signs nothing, so it needs no limits. Touchmark will still check that the code never loads a key or signs.
Treasury agent, with limits
The Safe treasury agent example on the home page. It holds a key from the environment, pays only listed suppliers, at most 500 XLM per payment and 1000 per day. Every one of those claims is enforced in its code, which is why it passes.
An MCP wallet server
A stdio MCP server for Soroban on testnet. It signs caller-supplied transactions with a secret the caller passes in, so it declares key_source: caller_supplied with holds_keys: false (1.1), recipients: any and human_approval: never: not reassuring, but true, and the audit can hold it to exactly that.
Contract tools, with 1.1 fields
An MCP server that creates one tool per function of a contract the caller registers. The family is one entry with generated: {"pattern": "invoke_*"}, so every invoke_… tool the code registers counts as declared. Its build tool runs the stellar CLI, so it says executes_code: true; the secret arrives with each call, so caller_supplied.
An SDK
A payments library is not an agent, but code built on it will hold keys and pay. Declaring the library's client and server roles as tools, with kind: sdk and network: both, tells integrators what each one signs, and the forbidden list states the guarantees it is checked against.
Common mistakes, and what the checker says
The checker answers by field, in these exact words. Any error and the file is not used at all: a half-read declaration could hide a limit or an authority.
A paying agent with no limits
Listing payments in signs (or a tool with moves_funds) and leaving limits out.
Add limits. If anyone can be paid without approval, say so: "recipients": "any", "human_approval": "never".
A key that both is and isn't held
holds_keys: true with key_source: "wallet_prompt", or false with env.
A signing tool, but nothing in signs
A tool has signs: true while authority.signs is empty.
Amounts as numbers, assets without issuers
"amount": 25, "25.00000001", or "asset": "USDC".
Quote amounts and name the issuer: "USDC:GBBD47…LFLA5" on testnet.
A typo in an address
A G… account with one wrong character fails its checksum.
Copy the address from Freighter rather than typing it.
A 1.1 field in a "1" file
caller_supplied, executes_code or generated in a file that still says "tmark": "1".
Change the version line to "tmark": "1.1". Nothing else changes.
A field the format doesn't have
spend_limit, description at the top level, or a field from an older file.
Smaller slips
A purpose under 20 characters, "tmark": 1 as a number, a cap on a tool that does not move funds, a value listed twice.
How a mismatch affects the verdict
Each claim is either kept by the code or broken by it. A kept claim is shown as declared. A broken one becomes a finding in the report, with the file and line that break it.
"recipients": "allowlist"
Is an allow-list enforced where the agent pays?
| Finding in the report | Severity | Effect |
|---|---|---|
| Declared testnet only, but the code can use the public network | high | An actionable risk: the decision is needs fixes, and the score is held at 64 or below. |
| The code has more Stellar authority than the .tmark declares | high | Actionable, as above. |
| A limit the .tmark declares is not enforced by the code | high | Actionable, as above. The evidence names each broken limit. |
| The .tmark file is not valid, so nothing in it was used | medium | Costs points; no claim, owner or limit in the file is used. |
| Declared mainnet, but the code only targets testnet | low | A small deduction: the declaration does not describe the code. |
| The repository uses an older declaration file | low | A nudge to migrate; older files cannot declare checked limits. |
New in 1.1
Three additions for agents that 1.0 could only describe in prose. They are optional and backward compatible: a "1" file keeps validating, and a file that uses any of them says "tmark": "1.1".
Where the key really comes from
caller_supplied for a secret passed in with each call; agent_generated for a key the agent creates and keeps.
"authority": {
"holds_keys": false,
"key_source": "caller_supplied",
"signs": ["contract_calls"]
}
Honest about a key the agent never keeps; the secret's path is still traced.
Shell and code execution
executes_code on a tool that runs a command, a build or an interpreter.
{ "name": "build_contract",
"description": "Compile with the stellar CLI",
"moves_funds": false, "signs": false,
"network": false, "files": true,
"executes_code": true }
Disclosed, not excused: what reaches the command is still assessed.
Generated tool families
generated declares every tool the code creates at run time with one entry.
{ "name": "invoke_function", …,
"generated": {
"pattern": "invoke_*",
"source": "each registered contract function"
} }
Each invoke_… tool in the code counts as declared, with this entry's flags.
Upgrading is one line: change "tmark": "1" to "1.1" when you add the first 1.1 field. python scripts/tmark.py init and the draft on the home page write "1.1" already.
Commit it, then test it.
Put the file at your repository root and run an audit: the report shows each claim the code keeps, and each one it breaks.