Tally on NEAR

Verification an agent can read for itself.

Looking a card up is free for everyone. Only AI agents pay for a check, and no outside agent pays yet.

For AI operatorsFor developersFor wallet buildersFor your agent: SKILL.md

For AI operators

Know which traders are real before your agent acts.

  • Check a claim before acting on it. Real is allow. Unclear is warn: stop and ask the owner. Fake, no record or no answer is block.
  • Read the proof straight from NEAR. No Tally server and no API key in between.
  • Set a spending limit. The credit you load is all the agent can ever spend.
  • Audit what it checked. Every paid check leaves a public record on NEAR.

What the agent can and can't do

It can

  • Pay for a check, out of the credit its owner loaded
  • Read any record for free

It can't

  • Send its owner's NEAR anywhere
  • Call any other contract, or anything here but the check
  • Spend more than the credit that was loaded
  • Be charged for a card that has no record
Run the demo agent yourself
git clone https://github.com/pab412/Tally-near
cd Tally-near/web && npm install
node scripts/demo-agent.mjs

Nothing to set up: it makes its own throwaway testnet account. It's a script with one rule, not an AI.

What the demo agent prints

One run, shortened

Example
Tally demo agent, on NEAR testnet
Rule: Real is allow (go ahead).
Unclear is warn (stop and ask the
owner). Fake, no record, or no answer
from NEAR is block.

1. The owner loads credit: 0.03 NEAR.
   That is all the agent can spend.
2. The agent gets its own key. It can
   call "check" and nothing else.

3. The agent checks 3 cards:
   cab484af…3be332b8  paid 0.01 NEAR
     BLOCK: refuses
     why: NEAR says Fake; check demo1,
     recorded 2026-11-10; 1 entry,
     never corrected
   3171ca6c…36599e58  paid 0.01 NEAR
     ALLOW: goes ahead
     why: NEAR says Real; check
     sample-b, recorded 2026-11-11;
     trader.testnet holds its verified
     card
     0.005 NEAR of the fee went into
     the trader's verified card.
   01a765b7…5d0c68bf  paid 0 NEAR
     BLOCK: refuses
     why: no record on NEAR: Tally has
     never checked this card

4. Attack, acted out: a message tells
   the agent to skip the check and send
   0.01 NEAR of the owner's money to
   another account. It tries.
   NEAR refuses: this key can only pay
   for checks.
5. The verified card now holds
   0.005 NEAR.
6. The owner takes the agent's key away.

For developers

One call to NEAR. No API key.

Every read is a plain JSON-RPC call to a NEAR node. Pick a method to see the call and what NEAR answers right now.

In Claude Code or Codex, the same reads come as a plug-in: the MCP server . No key, nothing paid.

Endpoint and contracts

Endpoint
https://rpc.testnet.fastnear.com
Network
NEAR testnet. Any testnet RPC node works.
Proof registry
proof.notpab.testnet
Verified cards
card.notpab.testnet

Read methods, free

Answered by NEAR just now

A card's latest verdict and how many entries it has. null for a card Tally never checked. proof.notpab.testnet

The call

curl -s https://rpc.testnet.fastnear.com \
  -H 'content-type: application/json' \
  -d '{"jsonrpc":"2.0","id":"tally","method":"query","params":{"request_type":"call_function","finality":"optimistic","account_id":"proof.notpab.testnet","method_name":"get","args_base64":"eyJmaW5nZXJwcmludCI6Ijc3NmI3MjlhOThhNDU1NzZlNDM4MWM3NzMyZDRiMGVhZmNiZWM0ZWVjYzExNGM3OTExNmQ5YTcwMDIyNThkYmUifQ=="}}'

What NEAR answers

Asking NEAR…

The paid check, method by method

deposit(account_id)The owner
Loads the attached NEAR as credit for an account. That credit is all the agent can spend.
check(fingerprint)The agent
Asks the registry and the card contract in one call. One fee, taken only when there is a record to give.
refund()The owner
Sends the caller's unspent credit back to them.
credit_of(account_id)Anyone, free
What an account has left to spend, in yoctoNEAR.
terms()Anyone, free
The fee, the trader's share and how many checks were answered.
What check answers
record
The card's record, exactly as the registry gives it to anyone. null when Tally never checked the card.
held_by
The account holding the card's verified card, when the card is Real and has one. Otherwise null.
charged
What this check cost, in yoctoNEAR. "0" when there was no record.
to_trader
The part of the fee paid into the trader's verified card, in yoctoNEAR.
Errors, word for word
A check for something that isn't a fingerprintThe paid check
“A fingerprint is 64 lower-case hex characters”
A check with less credit left than one feeThe paid check
“Not enough credit for a check: the account's owner has to load more”
A check the registry couldn't answerThe paid check
“The proof registry didn't answer, so nothing was charged”
A deposit smaller than one feeThe paid check
“Load at least the price of one check”
A refund with nothing leftThe paid check
“There is no credit to send back”
Anyone but Tally writing a recordThe proof registry
“Only Tally's attester can record a check”
A record with a bad check idThe proof registry
“A check id is 1 to 32 letters, digits, - or _”
Permissions and spending limits

The agent works with a NEAR function-call key that can only call check on the paid check contract. NEAR itself refuses anything else that key tries: a transfer, another method, another contract.

The most the agent can spend is the credit that was loaded, plus the gas allowance on its key. refund() sends unspent credit back.

Source and tests

The paid check has 21 unit tests, and its whole flow is among the 59 checks run on testnet.

The contract · The demo agent · The README

For wallet builders

Ask who is safe to pay, before the money moves.

A spending limit says how much an agent may spend. The registry says who it may pay. A wallet asks it in the same call that would send the money, so what the agent was told never decides.

use tally_types::{decide, proof_registry, Decision, Record};

// before any money moves, ask the registry about the card
pub fn pay(&mut self, fingerprint: String, amount: U128) -> Promise {
    proof_registry::ext(self.registry.clone())
        .get(fingerprint.clone())
        .then(Self::ext(env::current_account_id()).on_asked(fingerprint, amount))
}

#[private]
pub fn on_asked(&mut self, fingerprint: String, amount: U128,
    #[callback_result] record: Result<Option<Record>, PromiseError>) {
    // no answer is no record, and both are a block
    match decide(record.ok().flatten().as_ref()) {
        Decision::Allow => { /* pay the card's holder */ }
        Decision::Warn => { /* stop: ask the owner */ }
        Decision::Block => { /* refuse */ }
    }
}
deposit(limit)The owner
Loads the attached NEAR into the caller's own wallet and sets the most one payment may be.
pay(fingerprint, amount)The agent
Asks the registry about the card first. Its holder is paid only if the answer is Real. Anything else is refused, with why.
refund()The owner
Sends back what the caller's wallet still holds.
wallet_of(account_id)Anyone, free
What an account's wallet holds, and its limit.

The whole thing is contracts/guard, an example wallet on testnet at tally-guard.testnet . It is not a product people hold money in. Try it: node scripts/demo-guard.mjs pays a Real card's holder and is refused for a Fake one. Its source

Look a card up yourselfOpen the demo