Features Solutions Technology Tokenomics Docs About Launch App
Docs / NFC Integration / Encoding Guide

NFC Encoding Guide

Encoding a TAG IT tag means personalising an NXP NTAG 424 DNA chip for SUN/SDM, so that every physical tap emits a fresh, cryptographically authenticated URL. That is done with a PC/SC reader and low-level APDU tooling. There is no TAG IT encoding SDK, no encoding API and no hosted encoding service — this page documents the chip mechanism that is real, and says plainly where the tooling stops.

There is no TAG IT encoding SDK

Earlier revisions of this page showed new TagIt({ apiKey }) together with tagit.encoding.*, tagit.chips.*, tagit.crypto.* and tagit.alerts.* calls, plus /t/{TAG_ID} verification URLs. None of those exist. @tagit/sdk is not published to npm — the registry returns 404, so npm install @tagit/sdk fails — and the in-development package covers agent identity, reputation and validation only; it has no encoding, chip or key-management surface. The live system described below runs on Base Sepolia testnet (chain ID 84532) against unaudited contracts. Treat it as an evaluation environment, not a basis for a custody or settlement decision.

What exists today

Everything marked Live can be exercised right now. Everything marked Does not exist has no package, endpoint or method behind it, and nothing on this page pretends otherwise.

What you want to do How, today Status
Understand what a 424 DNA tap emits and why it is trustworthy This page plus NXP application note AN12196 Documented
Verify a physical tap server-side GET verify.tagit.network/api/verify?picc=&cmac= Live — needs a real tap
Fetch a Digital Product Passport credential for a tap GET verify.tagit.network/api/dpp/01/{gtin}/21/{serial}?picc=&cmac= Live — needs a real tap
Show a human-readable verification page verify.tagit.network/asset/{tokenId}, /tag/{uid}, /01/{gtin}/21/{serial} Live
Read an asset's owner and lifecycle state with no tag at all getAsset on TAGITCore via viem Live
Personalise a blank NTAG 424 DNA chip Internal PC/SC tooling driving an ACS ACR1252U reader Internal only — not distributed
Bind a chip to an asset on-chain, activate or flag it On-chain writes, gated by a BIDGES capability badge Not open to the public
Encoding sessions, batch-encoding API, key-management API, chip registry API, fraud-alert webhooks, certified encoding stations Do not exist

How an NTAG 424 DNA tap works

This part is genuine hardware behaviour, defined by NXP, and it is what makes a TAG IT tag hard to clone. The chip stores an NDEF URL, and in SDM (Secure Dynamic Messaging, marketed as SUN — Secure Unique NFC) mode it rewrites two regions of that URL on every single read, using AES keys that were written into the chip during personalisation and can never be read back out.

A tap therefore produces a URL like this, with different values each time:

https://verify.tagit.network/01/{GTIN}/21/{SERIAL}?picc=<32 hex>&cmac=<16 hex>

picc  16 bytes, AES-128-CBC encrypted (zero IV) under the chip's SDMMetaRead key.
      Plaintext = 0xC7 tag byte | 7-byte UID | 3-byte read counter (little-endian)
cmac  8 bytes, a truncated AES-CMAC computed under a per-tap session key that is
      itself derived from the chip's SDMFileRead key, its UID and the counter

Three consequences follow, and they are the whole security argument:

Verification is server-side by necessity, not by policy

Because the SDM keys are not on the reading device, nothing client-side can decide whether a tap is genuine. The mobile app forwards picc and cmac to the verifier and renders the answer. Any “offline verification” claim for this chip family would be false.

What is written to the chip

Personalisation writes a single NDEF URI record into the chip's NDEF file (file 2, 256 bytes of capacity on the 424 DNA — the chip's 416 bytes of user memory are split across the 32-byte CC file, this 256-byte NDEF file and a 128-byte proprietary file), with the picc and cmac parameters present as zero-filled placeholders. It then enables SDM and tells the chip the byte offsets of those two placeholders, so the chip knows where to inject the encrypted PICC data and the MAC at read time.

File 2 layout (NFC Forum Type 4 Tag NDEF file)

  bytes 0-1   NLEN, big-endian length of the NDEF message
  byte  2     0xD1   record header: MB=1, ME=1, SR=1, TNF=0x01 (well-known)
  byte  3     0x01   type length
  byte  4     payload length
  byte  5     'U'    record type: URI
  byte  6     0x04   URI identifier code: "https://"
  bytes 7-..  URI body:
              verify.tagit.network/...?picc=00000000000000000000000000000000
                                       &cmac=0000000000000000

The offsets of the two zero runs are passed to the chip as PICCDataOffset and
SDMMACOffset. The chip overwrites exactly those bytes on every read.

Nothing else is written. There is no metadata record, no signature record, no product JSON on the chip — the URL is the entire payload, and all product data is resolved from the chain and from off-chain metadata after the tap is verified.

Key layout

The 424 DNA has five application keys. TAG IT personalisation uses three of them:

KeyRoleValueHeld by
0 Application master (authentication) Left at the factory default in current tooling
1 SDMFileRead — computes the tap CMAC Diversified per chip from the master secret and the chip's UID Chip; re-derived by the verifier
2 SDMMetaRead — encrypts the PICC data The master secret itself, identical across the fleet Chip; verifier environment

Key 2 cannot be diversified per chip: the verifier has to decrypt picc before it knows which UID it is dealing with, so the decryption key must be fleet-wide. Key 1 is diversified, so compromising one chip's CMAC key does not forge taps for any other chip.

Current tooling does not lock the chip

Key 0 is deliberately left at its factory value so that personalisation is re-runnable and a chip can never be bricked during development. The trade-off is real and should be stated rather than glossed: anyone with physical access to a tag and a reader can re-run personalisation against it. Forging a valid tap for the TAG IT verifier still requires the master secret, which is not on the chip in recoverable form — but permanent configuration locking is not enabled today. Do not describe these tags as tamper-locked.

URL forms that resolve

Every route below is relative to the public verification host, verify.tagit.network. You can check an asset on-chain right now without a chip or a wallet.

Only the routes below exist. In particular, verify.tagit.network/t/{TAG_ID} and verify.tagit.network/{tagId} — both previously documented on this page — return 404. There is no 12-character alphanumeric tag identifier anywhere in the system; the on-chain tag identifier is keccak256 of the chip's raw 7-byte UID.

RouteReturns
/01/{gtin}/21/{serial}?picc=&cmac= GS1 Digital Link carrier — the form new chips are personalised with. Human-readable passport page for a verified tap.
/api/dpp/01/{gtin}/21/{serial}?picc=&cmac= The same passport as a W3C Verifiable Credential (VCDM 2.0 JSON-LD, UNTP Digital Product Passport). Unsigned in v1 — integrity comes from the on-chain anchor carried in evidence, and the response says so in an x-dpp-proof header.
/api/verify?picc=&cmac= Flat JSON verdict for a tap. Used by the mobile scanner.
/asset/{tokenId} Human-readable page for a digital twin, addressed by token ID. No tap required, so it proves the record exists — not that you are holding the product.
/tag/{uid} Human-readable page addressed by chip UID.
/sun?picc=&cmac= Legacy carrier, kept working for chips personalised before the GS1 form. Do not use it for new chips.

The GS1 GTIN and serial in the path are the standards-compliant identity layer. They are rendered alongside the result but they do not drive resolution — the UID recovered from the encrypted picc blob is the trust anchor, because it is the only part of the URL an attacker cannot choose.

Verifying a tap

The verifier runs this pipeline. Each step can fail, and the failure is reported rather than swallowed:

  1. Reject the request unless picc is 32 hex characters and cmac is 16.
  2. Decrypt picc with AES-128-CBC (zero IV) under the SDMMetaRead key.
  3. Check the first plaintext byte is 0xC7, the PICC data tag meaning “UID and counter present”. Read out the 7-byte UID and the 3-byte little-endian read counter.
  4. Derive the per-chip SDMFileRead key from the master secret and that UID, then derive the per-tap session key from the UID and counter.
  5. Recompute the truncated AES-CMAC and compare it to cmac in constant time. A mismatch means counterfeit, and the request stops here.
  6. Compute keccak256(UID) and call getTokenByTag on TAGITCore. A result of 0 means the chip is genuine but not yet bound to any asset.
  7. Call getAsset(tokenId) for the owner, timestamp and lifecycle state, and render the verdict.

The HTTP call is a plain GET. The picc and cmac values below are placeholders: only a genuine chip on a genuine tap can produce a pair that verifies, so this command is runnable only with values captured from real hardware.

curl "https://verify.tagit.network/api/verify?picc=<32 hex from the tap>&cmac=<16 hex from the tap>"

The response is a flat JSON object. Its fields:

FieldMeaning
verifiedBoolean. True only when the tap's crypto checks out and the asset is in lifecycle state 1–4.
boundWhether the chip resolves to an on-chain digital twin.
uidThe chip UID recovered from the encrypted blob, uppercase hex.
tapCounterThe chip's read counter for this tap.
assetPresent when bound: tokenId, stateCode, lifecycleState, owner, timestamp, and product fields (name, image, brand, sku, origin, msrp) from off-chain metadata.
metadataHashOn-chain metadata anchor, or null.
chain{ id: 84532, name: "Base Sepolia" }.
reason / errorWhy a tap failed. Missing parameters return 400; an unconfigured verifier returns 503; a chain read failure returns 502.
Replay detection is not implemented yet

The chip's counter is genuine and the verifier returns it as tapCounter, so a client that stores the highest counter it has seen per UID can detect a replayed URL itself. The hosted verifier does not currently persist a last-seen counter, does not reject a stale one, and raises no fraud alerts — there is no counter API and no alerting endpoint. If replay resistance matters to your integration, do that check on your side.

Reading the twin without a tap

Verifying presence needs the physical chip. Reading the asset record does not, and needs no credential of any kind. This was executed on 26 July 2026 and returns the values shown:

import { createPublicClient, http } from 'viem';
import { baseSepolia } from 'viem/chains';

const client = createPublicClient({ chain: baseSepolia, transport: http() });

const TAGITCoreABI = [{
  name: 'getAsset', type: 'function', stateMutability: 'view',
  inputs: [{ name: 'tokenId', type: 'uint256' }],
  outputs: [
    { name: 'assetOwner', type: 'address' },
    { name: 'timestamp',  type: 'uint64'  },
    { name: 'state',      type: 'uint8'   },
    { name: 'flags',      type: 'uint8'   },
    { name: 'reserved',   type: 'uint16'  },
  ],
}];

const [owner, timestamp, state] = await client.readContract({
  address: '0x3aDc7EFDb58Ae85483eFf5D4966D916185f31d1D',
  abi: TAGITCoreABI,
  functionName: 'getAsset',
  args: [50n],
});

console.log({ owner, timestamp, state });
// → { owner: '0x458B4d0c3a55006965Fd13D6af7B8509De51Cb3D',
//     timestamp: 1784246184n, state: 4 }   // 4 = CLAIMED

Lifecycle states

state and the stateCode field in the verifier response are the same integer. Branch on the number, not on a label:

CodeStateMeaning
0NONEToken does not exist
1MINTEDDigital twin created, no chip bound yet
2BOUNDNFC chip cryptographically bound
3ACTIVATEDQA passed, ready for distribution
4CLAIMEDOwned by an end consumer
5FLAGGEDReported lost, stolen or recalled
6RECYCLEDEnd of life, terminal

Programming a chip

Personalisation is a sequence of DESFire commands wrapped in ISO 7816 APDUs — authenticate EV2 First, ChangeKey for keys 1 and 2, WriteData for the NDEF file, ChangeFileSettings to enable SDM with the two offsets. It requires physical contact with a contactless reader, so it is inherently a local operation; no web API can perform it.

Hardware

Internal tooling — not publicly distributed

The personalisation implementation lives in the private tagit-nfc-bridge repository. It is a local daemon that binds to 127.0.0.1, exposes the reader to the admin console over an authenticated WebSocket, and performs the APDU sequence against whichever chip is on the antenna. It is not published to npm and not available for download, so the following is documentation of an internal interface, not something an external reader can run:

{
  "type": "personalize-sdm",
  "id": "<request id>",
  "baseUrl": "https://verify.tagit.network/01/<GTIN>/21/<SERIAL>"
}

The daemon appends the ?picc=…&cmac=… placeholders itself, computes the offsets and returns the exact URL template it wrote. The master secret is read from local configuration and never crosses the WebSocket.

If you need chips personalised for a pilot, that is currently done by the TAG IT team rather than self-served. Say so to anyone planning an integration timeline — there is no self-service encoding path today.

What does not exist

To be unambiguous, so that none of this reappears: there is no tagit.encoding, tagit.chips, tagit.crypto, tagit.alerts, tagit.nfc, tagit.tags, tagit.products or tagit.security namespace in any TAG IT package. There is no BatchEncoder, no encoding session, no createBatch, no generateKeys, no post-encoding verify call, no key-management service, no encoding permission on any API key — there are no API keys — no certified encoding station product, no fraud-alert webhook, no counter-anomaly endpoint, no sandbox host and no mainnet deployment. If you hold code written against any of those, it never ran. Replace tap verification with /api/verify and asset lookups with the direct on-chain read.

Troubleshooting

A tap does not verify

The reader is not responding

Need Help?

Join our Discord community, or see Chip Selection for the hardware decision and Contract Addresses for the deployment this page reads from.

Edit this page on GitHub
Type to search documentation...